summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorKyle D <kdumontnu@gmail.com>2022-09-02 15:18:23 -0400
committerGitHub <noreply@github.com>2022-09-02 15:18:23 -0400
commitc8ded77680db7344c8dc1ccee76bce0b4e02e103 (patch)
treebc63678ef62dc71ce68b29eeaf019c45cdb12034 /tests
parent5710ff343c9f16119ddbff06044e5d61388baa22 (diff)
downloadgitea-c8ded77680db7344c8dc1ccee76bce0b4e02e103.tar.gz
gitea-c8ded77680db7344c8dc1ccee76bce0b4e02e103.zip
Kd/ci playwright go test (#20123)
* Add initial playwright config * Simplify Makefile * Simplify Makefile * Use correct config files * Update playwright settings * Fix package-lock file * Don't use test logger for e2e tests * fix frontend lint * Allow passing TEST_LOGGER variable * Init postgres database * use standard gitea env variables * Update playwright * update drone * Move empty env var to commands * Cleanup * Move integrations to subfolder * tests integrations to tests integraton * Run e2e tests with go test * Fix linting * install CI deps * Add files to ESlint * Fix drone typo * Don't log to console in CI * Use go test http server * Add build step before tests * Move shared init function to common package * fix drone * Clean up tests * Fix linting * Better mocking for page + version string * Cleanup test generation * Remove dependency on gitea binary * Fix linting * add initial support for running specific tests * Add ACCEPT_VISUAL variable * don't require git-lfs * Add initial documentation * Review feedback * Add logged in session test * Attempt fixing drone race * Cleanup and bump version * Bump deps * Review feedback * simplify installation * Fix ci * Update install docs
Diffstat (limited to 'tests')
-rw-r--r--tests/e2e/README.md93
-rw-r--r--tests/e2e/e2e_test.go120
-rw-r--r--tests/e2e/example.test.e2e.js57
-rw-r--r--tests/e2e/utils_e2e.js60
-rw-r--r--tests/e2e/utils_e2e_test.go57
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/config6
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description1
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/74/8bf557dfc9c6457998b5118a6c8b2129f56c30bin0 -> 43 bytes
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/a5/46f86c7dd182592b96639045e176dde8df76efbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/b8/95782bd271fdd266dd06e5880ea4abdc3a0dc7bin0 -> 782 bytes
-rw-r--r--tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/config6
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description1
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/21/2f14c8b713de38bd0b3fb23bd288369b01668abin0 -> 43 bytes
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/90/e402c3937a4639725fcc59ca1f529e7dc8506fbin0 -> 783 bytes
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/ed/d9c1000cd1444efd63e153e3554c8d5656bf65bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/config7
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/description1
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-checkout3
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-commit3
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-merge3
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/hooks/pre-push3
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/indexbin0 -> 305 bytes
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/d6/f1/d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e01521
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/fb/8f/fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab0411
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/objects/54/6244003622c64b2fc3c2cd544d7a29882c8383bin0 -> 128 bytes
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/objects/6a/6ccf5d874fec134ee712572cc03a0f2dd7afecbin0 -> 51 bytes
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/objects/a6/7134b8484c2abe9fa954e1fd83b39b271383edbin0 -> 121 bytes
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/objects/b7/01ed6ffe410f0c3ac204b929ea47cfec6cef54bin0 -> 122 bytes
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/objects/f2/07b74f55cd7f9e800b7550d587cbc488f6eaf1bin0 -> 120 bytes
-rw-r--r--tests/gitea-repositories-meta/migration/lfs-test.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMITMESSAGE0
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMIT_EDITMSG1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/config10
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/config.backup7
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/description1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/indexbin0 -> 137 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/HEAD2
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/branch11
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9ebbin0 -> 60 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa3
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/branch11
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMITMESSAGE0
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMIT_EDITMSG1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config10
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config.backup7
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/indexbin0 -> 137 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/HEAD2
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/branch11
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9ebbin0 -> 60 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa3
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/branch11
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMITMESSAGE0
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMIT_EDITMSG1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config10
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config.backup7
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/indexbin0 -> 137 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/HEAD2
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/branch11
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9ebbin0 -> 60 bytes
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa3
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/branch11
-rw-r--r--tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/config6
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/description1
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/6e/75c9f89da9a9b93f4f36e61ed092f7a1625ba0bin0 -> 785 bytes
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/7f/eb6f9dd600e17a04f48a76cfa0a56a3f30e2c1bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/b7/91b41c0ae8cb3c4b12f3fd8c3709c2481d9e37bin0 -> 43 bytes
-rw-r--r--tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/config6
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/description1
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/04/f99c528b643b9175a4b156cdfc13aba6b43853bin0 -> 53 bytes
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/86/de16d8658f5c0a17ec6aa313871295d7072f78bin0 -> 43 bytes
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/bf/19fd4707acb403c4aca44f126ab69142ac59cebin0 -> 785 bytes
-rw-r--r--tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/config4
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user12/repo10.git/hooks/update7
-rwxr-xr-xtests/gitea-repositories-meta/user12/repo10.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/info/refs1
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349fbin0 -> 42 bytes
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4dbin0 -> 150 bytes
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/refs/heads/DefaultBranch1
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/refs/heads/develop1
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/refs/heads/feature/11
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user12/repo10.git/refs/tags/v1.11
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/config4
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user13/repo11.git/hooks/update7
-rwxr-xr-xtests/gitea-repositories-meta/user13/repo11.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/info/refs1
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/objects/0a/bcb056019adb8336cf9db3ad9d9cf80cd4b141bin0 -> 818 bytes
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349fbin0 -> 42 bytes
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4dbin0 -> 150 bytes
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/objects/75/d1afd00e111c8dbd9e3d96a27b431ac5ae6d74bin0 -> 44 bytes
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/objects/ed/447543e0c85d628b91f7f466f4921908f4c5eabin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/refs/heads/DefaultBranch1
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/refs/heads/branch21
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/refs/heads/develop1
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/refs/heads/feature/11
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user13/repo11.git/refs/tags/v1.11
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/config8
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/description1
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0a/8499a22ad32a80beda9d75efe15f9f945824682
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0c/cf1fcd4d1717c22de0707619a5577ea0acebf0bin0 -> 32 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3e/a192a6466793d4b7cd8641801ca0c6bec3979cbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3f/6594f108842b7c50772510e53ce113d3583c4abin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/58/e97d1a24fb9e1599d8a467ec409430f3d3569ebin0 -> 154 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/67/68c1fc1d9448422f05cc84267d94ee62085fe8bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/6e/8eabd9a7f8d6acd2a1219facfd37415564b1442
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/85/f46d747a68adf79cc01e2c25ba6a56932d298dbin0 -> 32 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/8d/dd8d1ad1fdc21ab629e906711fa9bc27aa1c52bin0 -> 32 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/95/fd0c4138480e4b3913e7cf71a90623fb926fe8bin0 -> 32 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/98/00fe78cabf4fe774fcf376f97fa2a0ed06987bbin0 -> 149 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/9f/cdb7d53bdef786d2e5577948a0c0d4b321fe5abin0 -> 32 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c2/0caf78b5f9dd2d0d183876c5cd0e761c13f7f82
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c5/2ba74685f5c8c593efbbb38f62fe024110adefbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/objects/d6/ae8023a10ff446b6a4e7f441554834008e99c3bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/packed-refs2
-rw-r--r--tests/gitea-repositories-meta/user2/commits_search_test.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive3
-rwxr-xr-xtests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update7
-rwxr-xr-xtests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/info/refs1
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349fbin0 -> 42 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4dbin0 -> 150 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/DefaultBranch1
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/develop1
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/feature/11
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/tags/v1.11
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/description1
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/48/06cb9df135782b818c968c2fadbd2c150d23d6bin0 -> 21 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/59/fee614e09d1f1cd1e15e4b2a7e9c8873a81498bin0 -> 34 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/7c/8ac2f8d82a1eb5f6aaece6629ff11015f91eb4bin0 -> 21 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/8e/592e636d27ac144f92f7fe8c33631cbdea594dbin0 -> 78 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/95/aff026f99a9ab76fbd01decb63dd3dbc03e498bin0 -> 34 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/ae/d1ffed24cc3cf9b80490795e893cae4bddd684bin0 -> 108 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/bf/d6a6583f9a9ac59bd726c1df26c64a89427edebin0 -> 21 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/c8/eb3b6c767ccb68411d0a1f6c769be69fb4d95a1
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/de/6be43fe8eb19ca3f4e934cb8b9a9a0b20fe865bin0 -> 50 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/objects/ef/6b814b610d8e7717aa0f71fbe5842bcf8146972
-rw-r--r--tests/gitea-repositories-meta/user2/glob.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo1.git/hooks/update7
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo1.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/info/refs3
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/00/750edc07d6415dcc07ae0351e9397b0222b7babin0 -> 17 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/3f/a2f829675543ecfc16b2891aebe8bf0608a8f4bin0 -> 138 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/4a/357436d925b5c974181ff12a994538ddc5a269bin0 -> 840 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349fbin0 -> 42 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2bin0 -> 833 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/5f/22f7d0d95d614d25a5b68592adb345a4b5c7fdbin0 -> 185 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07abin0 -> 839 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4dbin0 -> 150 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfbbin0 -> 86 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/90/dcd07da077d1e7cd6032b52d1f79ae2b5f19b22
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508eebin0 -> 842 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9abin0 -> 76 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1dbin0 -> 61 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7dddbin0 -> 20 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/d4/a1a6dcf7bd42891f264d484e80dac7e66b5410bin0 -> 34 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/d7/bd5b8cfb680f460e37b6fd7cf74c284e059118bin0 -> 85 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/dc/7a8ba127fee870dd683310ce660dfe59333a1bbin0 -> 78 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/dd/59742c0f6672911f2b64cba5711ac00593ed32bin0 -> 118 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/heads/DefaultBranch1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/heads/branch21
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/heads/develop1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/heads/feature/11
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/notes/commits1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.git/refs/tags/v1.11
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/description1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec32
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0d/ca5bd9b5d7ef937710e056f575e86c0184ba85bin0 -> 820 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/2c/54faec6c45d31c1abfaecdab471eac6633738abin0 -> 131 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3bin0 -> 63 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196bin0 -> 830 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc1
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20bin0 -> 94 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45ebin0 -> 53 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7bin0 -> 95 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/89/43a1d5f93c00439d5ffc0f8e36f5d60abae46cbin0 -> 206 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d62
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64bin0 -> 189 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e5/3d079e581fbfdea1075a54d5b621eab0090e52bin0 -> 52 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/ea/82fc8777a24b07c26b3a4bf4e2742c03733eabbin0 -> 44 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940bin0 -> 157 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/repo15.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/repo15.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/repo15.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo15.git/hooks/update7
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo15.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user2/repo15.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/description1
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/0c/3d59dea27b97aa3cb66072745d7a2c51a7a8b1bin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/24/f83a471f77579fea57bac7255d6e64e70fce1cbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/27/566bd5738fc8b4e3fef3c5e72cce608537bd95bin0 -> 575 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/3b/2b54fe3d9a8279d5b926124dccdf279b8eff2f1
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/45/8121ce9a6b855c9733bae62093caf3f39685debin0 -> 26 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/50/99b81332712fe655e34e8dd63574f503f618112
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/69/554a64c1e6030f051e5c3f94bfbd773cd6a324bin0 -> 158 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/a4/3476a501516e065c5a82f05fd58fd319598bc1bin0 -> 57 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/e9/4083fcdf1f10c545e9253a23c5e44a2ff68aacbin0 -> 524 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/f2/7c2b2b03dcab38beaf89b0ab4ff61f6de63441bin0 -> 522 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/objects/f9/0451c72ef61a7645293d17b47be7a8e983da57bin0 -> 27 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign1
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign-not-yet-validated1
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/repo16.git/refs/heads/not-signed1
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/description1
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/info/refs1
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/0a/7d8b41ae9763e9a1743917396839d1791d49d0bin0 -> 188 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec32
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c992787002
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/1c/887eaa8d81fa86da7695d8f635cf17813eb4221
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/26/f842bcad37fa40a1bb34cbb5ee219ee35d863dbin0 -> 75 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3bin0 -> 63 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/36/fff01c8c9f722d49d53186abd27b5be8d85338bin0 -> 155 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196bin0 -> 830 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc1
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20bin0 -> 94 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45ebin0 -> 53 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7bin0 -> 95 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/ba/1aed4e2ea2443d76cec241b96be4ec990852ecbin0 -> 117 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d62
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64bin0 -> 189 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940bin0 -> 157 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/info/commit-graphbin0 -> 1212 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/info/packs2
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.bitmapbin0 -> 248 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.idxbin0 -> 1240 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.packbin0 -> 637 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/packed-refs2
-rw-r--r--tests/gitea-repositories-meta/user2/repo2.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive15
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive15
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo20.git/hooks/update14
-rwxr-xr-xtests/gitea-repositories-meta/user2/repo20.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/info/refs1
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/02/15cbe13d2695a2c3464ab5e59f47f37c3ff5d5bin0 -> 21 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/05/81d7edf45206787ff93956ea892e8a2ae77604bin0 -> 47 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/1b/271d83842d348b1ee71d8e6ead400aaeb4d1b5bin0 -> 19 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/29/5ba6ac57fdd46f62a51272f40e60b6dea697b2bin0 -> 21 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/2c/ec0f7069ed09d934e904c49f414d8bdf818ce4bin0 -> 49 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/41/4a282859758ba7b159bfbd9c2b193eb8f135eebin0 -> 18 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/80/8038d2f71b0ab020991439cffd24309c7bc530bin0 -> 138 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/83/70977f63979e140b6b58992b1fdb4098b24cd9bin0 -> 104 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/8c/e1dee41e1a3700819a9a309f275f8dc7b7e0b6bin0 -> 154 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/ba/3aeafe10402c6b29535a58d91def7e43638d9dbin0 -> 22 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/c5/0ac6b9e25abb8200bb377755367d7265c581cfbin0 -> 75 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/ce/013625030ba8dba906f756967f9e9ca394464abin0 -> 21 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/db/89c972fc57862eae378f45b74aca228037d415bin0 -> 18 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user2/repo20.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/config4
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user2/utf8.git/hooks/update7
-rwxr-xr-xtests/gitea-repositories-meta/user2/utf8.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/info/refs9
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/14/c42687126acae9d1ad41d7bdb528f811065a6abin0 -> 39 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/1d/5e00f305a7ca6a8a94e65456820a6d260adab8bin0 -> 127 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/28/d579e4920fbf4f66e71dab3e779d9fbf41422a3
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/3a/810dbf6b96afaa8c5f69a8b6ec1dabfca7368bbin0 -> 176 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/3a/a73c3499bff049a352b4e265575373e964b89abin0 -> 137 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/3a/c6084110205f98174c4f1ec7e78cb21a15dfc2bin0 -> 23 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/4c/61dd0a799e0830e77edfe6c74f7c349bc8e62abin0 -> 40 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/50/4d9fe743979d4e9785a25a363c7007293f0838bin0 -> 40 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/56/92bcf9f7c9eacb1ad68442161f2573877f96f4bin0 -> 49 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/59/e2c41e8f5140bb0182acebec17c8ad9831cc62bin0 -> 847 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/64/89894ad11093fdc49c0ed857d80682344a7264bin0 -> 39 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/6d/0c79ce3401c67d1ad522e61c47083a9fdee16cbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/84/7c6d93c6860dd377651245711b7fbcd34a18d4bin0 -> 41 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/9b/9cc8f558d1c4f815592496fa24308ba2a9c824bin0 -> 47 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/a4/f1bb3f2f8c6a0e840e935812ef4903ce515dadbin0 -> 394 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/a9/a61830fbf4e84999d3b20cf178954366701fe5bin0 -> 129 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/c7/85b65bf16928b58567cb23669125c0ccd25a4fbin0 -> 44 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/e9/63733b8a355cf860c465b4af7b236a6ef08783bin0 -> 47 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/eb/f146f803fccbc1471ef01d8fa0fe12c14e61a51
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/ee/9686cb562f492f64381bff7f298b2a1c67a141bin0 -> 88 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/f4/02ff67c0b3161c3988dbf6188e6e0df257fd75bin0 -> 52 bytes
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Grüßen1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Plus+Is+Not+Space1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ГлавнаÑВетка1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/heads/а/б/в1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ブランãƒ1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/tags/Ð/人1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ТÑг1
-rw-r--r--tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ã‚¿ã‚°1
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/config6
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/description1
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive15
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive15
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/hooks/update14
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/info/refs1
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3bin0 -> 84 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2dbin0 -> 29 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061bin0 -> 121 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52bin0 -> 28 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75bin0 -> 154 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaebbin0 -> 53 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6fbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive1
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/config6
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/description1
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive15
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive15
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/hooks/update14
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/info/refs1
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3bin0 -> 84 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2dbin0 -> 29 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061bin0 -> 121 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52bin0 -> 28 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75bin0 -> 154 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaebbin0 -> 53 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6fbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/objects/info/packs1
-rw-r--r--tests/gitea-repositories-meta/user27/template1.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/config6
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/description1
-rwxr-xr-xtests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive7
-rwxr-xr-xtests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive.d/gitea2
-rwxr-xr-xtests/gitea-repositories-meta/user3/repo3.git/hooks/update7
-rwxr-xr-xtests/gitea-repositories-meta/user3/repo3.git/hooks/update.d/gitea2
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/20/ade30d25e0ecaeec84e7f542a8456900858240bin0 -> 84 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/27/74debeea6dc742cc4971a92db0e08b95b60588bin0 -> 51 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/2a/47ca4b614a9f5a43abbd5ad851a54a616ffee6bin0 -> 760 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/2f/9b22fd3159a43b7b4e5dd806fcd544edf8716fbin0 -> 37 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/d2/2b4d4daa5be07329fcef6ed458f00cf3392da0bin0 -> 814 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/d5/6a3073c1dbb7b15963110a049d50cdb5db99fcbin0 -> 42 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/ec/f0db3c1ec806522de4b491fb9a3c7457398c61bin0 -> 62 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/objects/ee/16d127df463aa491e08958120f2108b02468dfbin0 -> 84 bytes
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/refs/heads/master1
-rw-r--r--tests/gitea-repositories-meta/user3/repo3.git/refs/heads/test_branch1
-rw-r--r--tests/gitea-repositories-meta/user30/empty.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user30/empty.git/config6
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/HEAD1
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/config4
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/description1
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/info/exclude6
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/objects/16/dfebd1ed3905d78d7e061e945fc9c34afe4e81bin0 -> 24 bytes
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/objects/c1/202ad022ae7d3a6d2474dc76d5a0c8e87cdc0fbin0 -> 54 bytes
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/objects/c7/cd3cd144e6d23c9d6f3d07e52b2c1a956e0338bin0 -> 818 bytes
-rw-r--r--tests/gitea-repositories-meta/user5/repo4.git/refs/heads/master1
-rw-r--r--tests/integration/README.md96
-rw-r--r--tests/integration/README_ZH.md71
-rw-r--r--tests/integration/admin_user_test.go84
-rw-r--r--tests/integration/api_activitypub_person_test.go113
-rw-r--r--tests/integration/api_admin_org_test.go88
-rw-r--r--tests/integration/api_admin_test.go211
-rw-r--r--tests/integration/api_branch_test.go201
-rw-r--r--tests/integration/api_comment_test.go205
-rw-r--r--tests/integration/api_fork_test.go19
-rw-r--r--tests/integration/api_gpg_keys_test.go264
-rw-r--r--tests/integration/api_helper_for_declarative_test.go464
-rw-r--r--tests/integration/api_httpsig_test.go138
-rw-r--r--tests/integration/api_issue_label_test.go208
-rw-r--r--tests/integration/api_issue_milestone_test.go81
-rw-r--r--tests/integration/api_issue_reaction_test.go144
-rw-r--r--tests/integration/api_issue_stopwatch_test.go92
-rw-r--r--tests/integration/api_issue_subscription_test.go77
-rw-r--r--tests/integration/api_issue_test.go329
-rw-r--r--tests/integration/api_issue_tracked_time_test.go125
-rw-r--r--tests/integration/api_keys_test.go201
-rw-r--r--tests/integration/api_nodeinfo_test.go39
-rw-r--r--tests/integration/api_notification_test.go193
-rw-r--r--tests/integration/api_oauth2_apps_test.go166
-rw-r--r--tests/integration/api_org_test.go153
-rw-r--r--tests/integration/api_packages_composer_test.go215
-rw-r--r--tests/integration/api_packages_conan_test.go725
-rw-r--r--tests/integration/api_packages_container_test.go608
-rw-r--r--tests/integration/api_packages_generic_test.go194
-rw-r--r--tests/integration/api_packages_helm_test.go167
-rw-r--r--tests/integration/api_packages_maven_test.go218
-rw-r--r--tests/integration/api_packages_npm_test.go279
-rw-r--r--tests/integration/api_packages_nuget_test.go393
-rw-r--r--tests/integration/api_packages_pub_test.go180
-rw-r--r--tests/integration/api_packages_pypi_test.go182
-rw-r--r--tests/integration/api_packages_rubygems_test.go227
-rw-r--r--tests/integration/api_packages_test.go168
-rw-r--r--tests/integration/api_packages_vagrant_test.go171
-rw-r--r--tests/integration/api_private_serv_test.go154
-rw-r--r--tests/integration/api_pull_commits_test.go41
-rw-r--r--tests/integration/api_pull_review_test.go307
-rw-r--r--tests/integration/api_pull_test.go185
-rw-r--r--tests/integration/api_releases_test.go233
-rw-r--r--tests/integration/api_repo_archive_test.go54
-rw-r--r--tests/integration/api_repo_collaborator_test.go131
-rw-r--r--tests/integration/api_repo_edit_test.go346
-rw-r--r--tests/integration/api_repo_file_create_test.go308
-rw-r--r--tests/integration/api_repo_file_delete_test.go170
-rw-r--r--tests/integration/api_repo_file_get_test.go57
-rw-r--r--tests/integration/api_repo_file_helpers.go29
-rw-r--r--tests/integration/api_repo_file_update_test.go278
-rw-r--r--tests/integration/api_repo_get_contents_list_test.go167
-rw-r--r--tests/integration/api_repo_get_contents_test.go163
-rw-r--r--tests/integration/api_repo_git_blobs_test.go79
-rw-r--r--tests/integration/api_repo_git_commits_test.go152
-rw-r--r--tests/integration/api_repo_git_hook_test.go197
-rw-r--r--tests/integration/api_repo_git_notes_test.go41
-rw-r--r--tests/integration/api_repo_git_ref_test.go36
-rw-r--r--tests/integration/api_repo_git_tags_test.go88
-rw-r--r--tests/integration/api_repo_git_trees_test.go77
-rw-r--r--tests/integration/api_repo_languages_test.go50
-rw-r--r--tests/integration/api_repo_lfs_locks_test.go181
-rw-r--r--tests/integration/api_repo_lfs_migrate_test.go54
-rw-r--r--tests/integration/api_repo_lfs_test.go487
-rw-r--r--tests/integration/api_repo_raw_test.go38
-rw-r--r--tests/integration/api_repo_tags_test.go84
-rw-r--r--tests/integration/api_repo_teams_test.go82
-rw-r--r--tests/integration/api_repo_test.go694
-rw-r--r--tests/integration/api_repo_topic_test.go155
-rw-r--r--tests/integration/api_settings_test.go65
-rw-r--r--tests/integration/api_team_test.go268
-rw-r--r--tests/integration/api_team_user_test.go46
-rw-r--r--tests/integration/api_token_test.go66
-rw-r--r--tests/integration/api_user_email_test.go112
-rw-r--r--tests/integration/api_user_heatmap_test.go39
-rw-r--r--tests/integration/api_user_org_perm_test.go151
-rw-r--r--tests/integration/api_user_orgs_test.go121
-rw-r--r--tests/integration/api_user_search_test.go94
-rw-r--r--tests/integration/api_wiki_test.go252
-rw-r--r--tests/integration/attachment_test.go134
-rw-r--r--tests/integration/auth_ldap_test.go415
-rw-r--r--tests/integration/benchmarks_test.go72
-rw-r--r--tests/integration/branches_test.go76
-rw-r--r--tests/integration/change_default_branch_test.go41
-rw-r--r--tests/integration/cmd_keys_test.go65
-rw-r--r--tests/integration/compare_test.go42
-rw-r--r--tests/integration/cors_test.go23
-rw-r--r--tests/integration/create_no_session_test.go120
-rw-r--r--tests/integration/csrf_test.go53
-rw-r--r--tests/integration/delete_user_test.go61
-rw-r--r--tests/integration/download_test.go94
-rw-r--r--tests/integration/dump_restore_test.go328
-rw-r--r--tests/integration/editor_test.go164
-rw-r--r--tests/integration/empty_repo_test.go31
-rw-r--r--tests/integration/eventsource_test.go83
-rw-r--r--tests/integration/explore_repos_test.go19
-rw-r--r--tests/integration/git_clone_wiki_test.go53
-rw-r--r--tests/integration/git_helper_for_declarative_test.go202
-rw-r--r--tests/integration/git_smart_http_test.go69
-rw-r--r--tests/integration/git_test.go851
-rw-r--r--tests/integration/goget_test.go36
-rw-r--r--tests/integration/gpg_git_test.go369
-rw-r--r--tests/integration/html_helper.go60
-rw-r--r--tests/integration/integration_test.go407
-rw-r--r--tests/integration/issue_test.go559
-rw-r--r--tests/integration/lfs_getobject_test.go186
-rw-r--r--tests/integration/lfs_local_endpoint_test.go117
-rw-r--r--tests/integration/links_test.go176
-rw-r--r--tests/integration/migrate_test.go98
-rw-r--r--tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gzbin0 -> 12969 bytes
-rw-r--r--tests/integration/migration-test/gitea-v1.6.4.mysql.sql.gzbin0 -> 9423 bytes
-rw-r--r--tests/integration/migration-test/gitea-v1.6.4.postgres.sql.gzbin0 -> 17517 bytes
-rw-r--r--tests/integration/migration-test/gitea-v1.6.4.sqlite3.sql.gzbin0 -> 3995 bytes
-rw-r--r--tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gzbin0 -> 13068 bytes
-rw-r--r--tests/integration/migration-test/gitea-v1.7.0.mysql.sql.gzbin0 -> 9682 bytes
-rw-r--r--tests/integration/migration-test/gitea-v1.7.0.postgres.sql.gzbin0 -> 17831 bytes
-rw-r--r--tests/integration/migration-test/gitea-v1.7.0.sqlite3.sql.gzbin0 -> 8165 bytes
-rw-r--r--tests/integration/migration-test/migration_test.go338
-rw-r--r--tests/integration/mirror_pull_test.go98
-rw-r--r--tests/integration/mirror_push_test.go120
-rw-r--r--tests/integration/nonascii_branches_test.go214
-rw-r--r--tests/integration/oauth_test.go261
-rw-r--r--tests/integration/org_count_test.go147
-rw-r--r--tests/integration/org_test.go224
-rw-r--r--tests/integration/private-testing.key81
-rw-r--r--tests/integration/privateactivity_test.go417
-rw-r--r--tests/integration/pull_compare_test.go28
-rw-r--r--tests/integration/pull_create_test.go163
-rw-r--r--tests/integration/pull_merge_test.go423
-rw-r--r--tests/integration/pull_review_test.go22
-rw-r--r--tests/integration/pull_status_test.go157
-rw-r--r--tests/integration/pull_update_test.go174
-rw-r--r--tests/integration/release_test.go213
-rw-r--r--tests/integration/rename_branch_test.go45
-rw-r--r--tests/integration/repo_activity_test.go66
-rw-r--r--tests/integration/repo_branch_test.go148
-rw-r--r--tests/integration/repo_commits_search_test.go43
-rw-r--r--tests/integration/repo_commits_test.go117
-rw-r--r--tests/integration/repo_fork_test.go76
-rw-r--r--tests/integration/repo_generate_test.go69
-rw-r--r--tests/integration/repo_migrate_test.go46
-rw-r--r--tests/integration/repo_search_test.go63
-rw-r--r--tests/integration/repo_tag_test.go99
-rw-r--r--tests/integration/repo_test.go184
-rw-r--r--tests/integration/repo_topic_test.go48
-rw-r--r--tests/integration/repo_watch_test.go25
-rw-r--r--tests/integration/repofiles_delete_test.go202
-rw-r--r--tests/integration/repofiles_update_test.go416
-rw-r--r--tests/integration/setting_test.go108
-rw-r--r--tests/integration/signin_test.go60
-rw-r--r--tests/integration/signout_test.go28
-rw-r--r--tests/integration/signup_test.go94
-rw-r--r--tests/integration/ssh_key_test.go214
-rw-r--r--tests/integration/timetracking_test.go82
-rw-r--r--tests/integration/user_avatar_test.go82
-rw-r--r--tests/integration/user_test.go251
-rw-r--r--tests/integration/version_test.go28
-rw-r--r--tests/integration/view_test.go27
-rw-r--r--tests/integration/webfinger_test.go69
-rw-r--r--tests/integration/xss_test.go40
-rw-r--r--tests/mssql.ini.tmpl110
-rw-r--r--tests/mysql.ini.tmpl126
-rw-r--r--tests/mysql8.ini.tmpl107
-rw-r--r--tests/pgsql.ini.tmpl111
-rw-r--r--tests/sqlite.ini.tmpl109
-rw-r--r--tests/test_utils.go224
-rw-r--r--tests/testlogger.go207
628 files changed, 26871 insertions, 0 deletions
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
new file mode 100644
index 0000000000..c84d7807fc
--- /dev/null
+++ b/tests/e2e/README.md
@@ -0,0 +1,93 @@
+# End to end tests
+
+E2e tests largely follow the same syntax as [integration tests](tests/e2e/README.md).
+Whereas integration tests are intended to mock and stress the back-end, server-side code, e2e tests the interface between front-end and back-end, as well as visual regressions with both assertions and visual comparisons.
+They can be run with make commands for the appropriate backends, namely:
+```shell
+make test-sqlite
+make test-pgsql
+make test-mysql
+make test-mysql8
+make test-mssql
+```
+
+Make sure to perform a clean front-end build before running tests:
+```
+make clean frontend
+```
+
+## Install playwright system dependencies
+```
+npx playwright install-deps
+```
+
+
+## Run all tests via local drone
+```
+drone exec --local --build-event "pull_request"
+```
+
+## Run sqlite e2e tests
+Start tests
+```
+make test-e2e-sqlite
+```
+
+## Run MySQL e2e tests
+Setup a MySQL database inside docker
+```
+docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest #(just ctrl-c to stop db and clean the container)
+docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --rm --name elasticsearch elasticsearch:7.6.0 #(in a second terminal, just ctrl-c to stop db and clean the container)
+```
+Start tests based on the database container
+```
+TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-e2e-mysql
+```
+
+## Run pgsql e2e tests
+Setup a pgsql database inside docker
+```
+docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:latest #(just ctrl-c to stop db and clean the container)
+```
+Start tests based on the database container
+```
+TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-e2e-pgsql
+```
+
+## Run mssql e2e tests
+Setup a mssql database inside docker
+```
+docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
+```
+Start tests based on the database container
+```
+TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql
+```
+
+## Running individual tests
+
+Example command to run `example.test.e2e.js` test file:
+
+_Note: unlike integration tests, this filtering is at the file level, not function_
+
+For SQLite:
+
+```
+make test-e2e-sqlite#example
+```
+
+For other databases(replace `mssql` to `mysql`, `mysql8` or `pgsql`):
+
+```
+TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example
+```
+
+## Visual testing
+
+Although the main goal of e2e is assertion testing, we have added a framework for visual regress testing. If you are working on front-end features, please use the following:
+ - Check out `main`, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1` to generate outputs. This will initially fail, as no screenshots exist. You can run the e2e tests again to assert it passes.
+ - Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert you front-end changes don't break any other tests unintentionally.
+
+VISUAL_TEST=1 will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
+
+ACCEPT_VISUAL=1 will overwrite the snapshot images with new images. \ No newline at end of file
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
new file mode 100644
index 0000000000..c77c071181
--- /dev/null
+++ b/tests/e2e/e2e_test.go
@@ -0,0 +1,120 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+// This is primarily coped from /tests/integration/integration_test.go
+// TODO: Move common functions to shared file
+
+package e2e
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers"
+ "code.gitea.io/gitea/tests"
+)
+
+var c *web.Route
+
+func TestMain(m *testing.M) {
+ defer log.Close()
+
+ managerCtx, cancel := context.WithCancel(context.Background())
+ graceful.InitManager(managerCtx)
+ defer cancel()
+
+ tests.InitTest(false)
+ c = routers.NormalRoutes(context.TODO())
+
+ os.Unsetenv("GIT_AUTHOR_NAME")
+ os.Unsetenv("GIT_AUTHOR_EMAIL")
+ os.Unsetenv("GIT_AUTHOR_DATE")
+ os.Unsetenv("GIT_COMMITTER_NAME")
+ os.Unsetenv("GIT_COMMITTER_EMAIL")
+ os.Unsetenv("GIT_COMMITTER_DATE")
+
+ err := unittest.InitFixtures(
+ unittest.FixturesOptions{
+ Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
+ },
+ )
+ if err != nil {
+ fmt.Printf("Error initializing test database: %v\n", err)
+ os.Exit(1)
+ }
+
+ exitVal := m.Run()
+
+ tests.WriterCloser.Reset()
+
+ if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
+ fmt.Printf("util.RemoveAll: %v\n", err)
+ os.Exit(1)
+ }
+ if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
+ fmt.Printf("Unable to remove repo indexer: %v\n", err)
+ os.Exit(1)
+ }
+
+ os.Exit(exitVal)
+}
+
+// This should be the only test e2e necessary. It will collect all "*.test.e2e.js"
+// files in this directory and build a test for each.
+func TestE2e(t *testing.T) {
+ // Find the paths of all e2e test files in test test directory.
+ searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js")
+ paths, err := filepath.Glob(searchGlob)
+ if err != nil {
+ t.Fatal(err)
+ } else if len(paths) == 0 {
+ t.Fatal(fmt.Errorf("No e2e tests found in %s", searchGlob))
+ }
+
+ runArgs := []string{"npx", "playwright", "test"}
+
+ // To update snapshot outputs
+ if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
+ runArgs = append(runArgs, "--update-snapshots")
+ }
+
+ // Create new test for each input file
+ for _, path := range paths {
+ _, filename := filepath.Split(path)
+ testname := filename[:len(filename)-len(filepath.Ext(path))]
+
+ t.Run(testname, func(t *testing.T) {
+ // Default 2 minute timeout
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ cmd := exec.Command(runArgs[0], runArgs...)
+ cmd.Env = os.Environ()
+ cmd.Env = append(cmd.Env, fmt.Sprintf("GITEA_URL=%s", setting.AppURL))
+ var stdout, stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ err := cmd.Run()
+ if err != nil {
+ // Currently colored output is conflicting. Using Printf until that is resolved.
+ fmt.Printf("%v", stdout.String())
+ fmt.Printf("%v", stderr.String())
+ log.Fatal("Playwright Failed: %s", err)
+ } else {
+ fmt.Printf("%v", stdout.String())
+ }
+ })
+ })
+ }
+}
diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.js
new file mode 100644
index 0000000000..bd19ceb8fc
--- /dev/null
+++ b/tests/e2e/example.test.e2e.js
@@ -0,0 +1,57 @@
+// @ts-check
+import {test, expect} from '@playwright/test';
+import {login_user, save_visual, load_logged_in_context} from './utils_e2e.js';
+
+test.beforeAll(async ({browser}, workerInfo) => {
+ await login_user(browser, workerInfo, 'user2');
+});
+
+test('Load Homepage', async ({page}) => {
+ const response = await page.goto('/');
+ await expect(response?.status()).toBe(200); // Status OK
+ await expect(page).toHaveTitle(/^Gitea: Git with a cup of tea\s*$/);
+ await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg');
+});
+
+test('Test Register Form', async ({page}, workerInfo) => {
+ const response = await page.goto('/user/sign_up');
+ await expect(response?.status()).toBe(200); // Status OK
+ await page.type('input[name=user_name]', `e2e-test-${workerInfo.workerIndex}`);
+ await page.type('input[name=email]', `e2e-test-${workerInfo.workerIndex}@test.com`);
+ await page.type('input[name=password]', 'test123');
+ await page.type('input[name=retype]', 'test123');
+ await page.click('form button.ui.green.button:visible');
+ // Make sure we routed to the home page. Else login failed.
+ await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
+ await expect(page.locator('.dashboard-navbar span>img.ui.avatar.image')).toBeVisible();
+ await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created.');
+
+ save_visual(page);
+});
+
+test('Test Login Form', async ({page}, workerInfo) => {
+ const response = await page.goto('/user/login');
+ await expect(response?.status()).toBe(200); // Status OK
+
+ await page.type('input[name=user_name]', `user2`);
+ await page.type('input[name=password]', `password`);
+ await page.click('form button.ui.green.button:visible');
+
+ await page.waitForLoadState('networkidle');
+
+ await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
+
+ save_visual(page);
+});
+
+test('Test Logged In User', async ({browser}, workerInfo) => {
+ const context = await load_logged_in_context(browser, workerInfo, 'user2');
+ const page = await context.newPage();
+
+ await page.goto('/');
+
+ // Make sure we routed to the home page. Else login failed.
+ await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
+
+ save_visual(page);
+});
diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.js
new file mode 100644
index 0000000000..b5b9ce2751
--- /dev/null
+++ b/tests/e2e/utils_e2e.js
@@ -0,0 +1,60 @@
+import {expect} from '@playwright/test';
+
+const ARTIFACTS_PATH = `tests/e2e/test-artifacts`;
+const LOGIN_PASSWORD = 'password';
+
+// log in user and store session info. This should generally be
+// run in test.beforeAll(), then the session can be loaded in tests.
+export async function login_user(browser, workerInfo, user) {
+ // Set up a new context
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ // Route to login page
+ // Note: this could probably be done more quickly with a POST
+ const response = await page.goto('/user/login');
+ await expect(response?.status()).toBe(200); // Status OK
+
+ // Fill out form
+ await page.type('input[name=user_name]', user);
+ await page.type('input[name=password]', LOGIN_PASSWORD);
+ await page.click('form button.ui.green.button:visible');
+
+ await page.waitForLoadState('networkidle');
+
+ await expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`);
+
+ // Save state
+ await context.storageState({path: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
+
+ return context;
+}
+
+export async function load_logged_in_context(browser, workerInfo, user) {
+ let context;
+ try {
+ context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`);
+ }
+ }
+ return context;
+}
+
+export async function save_visual(page) {
+ // Optionally include visual testing
+ if (process.env.VISUAL_TEST) {
+ await page.waitForLoadState('networkidle');
+ // Mock page/version string
+ await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK');
+ await expect(page).toHaveScreenshot({
+ fullPage: true,
+ timeout: 20000,
+ mask: [
+ page.locator('.dashboard-navbar span>img.ui.avatar.image'),
+ page.locator('.ui.dropdown.jump.item.tooltip span>img.ui.avatar.image'),
+ ],
+ });
+ }
+}
diff --git a/tests/e2e/utils_e2e_test.go b/tests/e2e/utils_e2e_test.go
new file mode 100644
index 0000000000..f8f3e94cda
--- /dev/null
+++ b/tests/e2e/utils_e2e_test.go
@@ -0,0 +1,57 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package e2e
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func onGiteaRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
+ if len(prepare) == 0 || prepare[0] {
+ defer tests.PrepareTestEnv(t, 1)()
+ }
+ s := http.Server{
+ Handler: c,
+ }
+
+ u, err := url.Parse(setting.AppURL)
+ assert.NoError(t, err)
+ listener, err := net.Listen("tcp", u.Host)
+ i := 0
+ for err != nil && i <= 10 {
+ time.Sleep(100 * time.Millisecond)
+ listener, err = net.Listen("tcp", u.Host)
+ i++
+ }
+ assert.NoError(t, err)
+ u.Host = listener.Addr().String()
+
+ defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ s.Shutdown(ctx)
+ cancel()
+ }()
+
+ go s.Serve(listener)
+ // Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
+
+ callback(t, u)
+}
+
+func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...bool) {
+ onGiteaRunTB(t, func(t testing.TB, u *url.URL) {
+ callback(t.(*testing.T), u)
+ }, prepare...)
+}
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/HEAD b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/config b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/config
new file mode 100644
index 0000000000..e6da231579
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
+ ignorecase = true
+ precomposeunicode = true
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/74/8bf557dfc9c6457998b5118a6c8b2129f56c30 b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/74/8bf557dfc9c6457998b5118a6c8b2129f56c30
new file mode 100644
index 0000000000..980093a921
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/74/8bf557dfc9c6457998b5118a6c8b2129f56c30
Binary files differ
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/a5/46f86c7dd182592b96639045e176dde8df76ef b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/a5/46f86c7dd182592b96639045e176dde8df76ef
new file mode 100644
index 0000000000..b8b1449445
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/a5/46f86c7dd182592b96639045e176dde8df76ef
Binary files differ
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/b8/95782bd271fdd266dd06e5880ea4abdc3a0dc7 b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/b8/95782bd271fdd266dd06e5880ea4abdc3a0dc7
new file mode 100644
index 0000000000..6b6d55bf98
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/b8/95782bd271fdd266dd06e5880ea4abdc3a0dc7
Binary files differ
diff --git a/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/refs/heads/master b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/refs/heads/master
new file mode 100644
index 0000000000..70132ddec1
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/refs/heads/master
@@ -0,0 +1 @@
+b895782bd271fdd266dd06e5880ea4abdc3a0dc7
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/HEAD b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/config b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/config
new file mode 100644
index 0000000000..e6da231579
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
+ ignorecase = true
+ precomposeunicode = true
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/21/2f14c8b713de38bd0b3fb23bd288369b01668a b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/21/2f14c8b713de38bd0b3fb23bd288369b01668a
new file mode 100644
index 0000000000..45a0732001
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/21/2f14c8b713de38bd0b3fb23bd288369b01668a
Binary files differ
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/90/e402c3937a4639725fcc59ca1f529e7dc8506f b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/90/e402c3937a4639725fcc59ca1f529e7dc8506f
new file mode 100644
index 0000000000..b73b5a5c0f
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/90/e402c3937a4639725fcc59ca1f529e7dc8506f
Binary files differ
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/ed/d9c1000cd1444efd63e153e3554c8d5656bf65 b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/ed/d9c1000cd1444efd63e153e3554c8d5656bf65
new file mode 100644
index 0000000000..f69006315e
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/ed/d9c1000cd1444efd63e153e3554c8d5656bf65
Binary files differ
diff --git a/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/refs/heads/master b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/refs/heads/master
new file mode 100644
index 0000000000..9de5b8d826
--- /dev/null
+++ b/tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/refs/heads/master
@@ -0,0 +1 @@
+90e402c3937a4639725fcc59ca1f529e7dc8506f
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/HEAD b/tests/gitea-repositories-meta/migration/lfs-test.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/config b/tests/gitea-repositories-meta/migration/lfs-test.git/config
new file mode 100644
index 0000000000..3f8f41b6b4
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/config
@@ -0,0 +1,7 @@
+[core]
+ bare = false
+ repositoryformatversion = 0
+ filemode = false
+ symlinks = false
+ ignorecase = true
+ logallrefupdates = true
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/description b/tests/gitea-repositories-meta/migration/lfs-test.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-checkout b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-checkout
new file mode 100644
index 0000000000..cab40f2649
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-checkout
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout.\n"; exit 2; }
+git lfs post-checkout "$@"
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-commit b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-commit
new file mode 100644
index 0000000000..9443f4161a
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-commit
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-commit.\n"; exit 2; }
+git lfs post-commit "$@"
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-merge b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-merge
new file mode 100644
index 0000000000..828b70891e
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-merge
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-merge.\n"; exit 2; }
+git lfs post-merge "$@"
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/pre-push b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/pre-push
new file mode 100644
index 0000000000..81a9cc6398
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/hooks/pre-push
@@ -0,0 +1,3 @@
+#!/bin/sh
+command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/pre-push.\n"; exit 2; }
+git lfs pre-push "$@"
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/index b/tests/gitea-repositories-meta/migration/lfs-test.git/index
new file mode 100644
index 0000000000..13f8e26966
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/index
Binary files differ
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/d6/f1/d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0152 b/tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/d6/f1/d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0152
new file mode 100644
index 0000000000..e9b0a4e8a0
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/d6/f1/d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0152
@@ -0,0 +1 @@
+dummy2 \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/fb/8f/fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041 b/tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/fb/8f/fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041
new file mode 100644
index 0000000000..71676cd9c0
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/fb/8f/fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041
@@ -0,0 +1 @@
+dummy1 \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/objects/54/6244003622c64b2fc3c2cd544d7a29882c8383 b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/54/6244003622c64b2fc3c2cd544d7a29882c8383
new file mode 100644
index 0000000000..0db52afbca
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/54/6244003622c64b2fc3c2cd544d7a29882c8383
Binary files differ
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/objects/6a/6ccf5d874fec134ee712572cc03a0f2dd7afec b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/6a/6ccf5d874fec134ee712572cc03a0f2dd7afec
new file mode 100644
index 0000000000..8a96927e57
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/6a/6ccf5d874fec134ee712572cc03a0f2dd7afec
Binary files differ
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/objects/a6/7134b8484c2abe9fa954e1fd83b39b271383ed b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/a6/7134b8484c2abe9fa954e1fd83b39b271383ed
new file mode 100644
index 0000000000..122f87efcc
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/a6/7134b8484c2abe9fa954e1fd83b39b271383ed
Binary files differ
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/objects/b7/01ed6ffe410f0c3ac204b929ea47cfec6cef54 b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/b7/01ed6ffe410f0c3ac204b929ea47cfec6cef54
new file mode 100644
index 0000000000..554b7f05b0
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/b7/01ed6ffe410f0c3ac204b929ea47cfec6cef54
Binary files differ
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/objects/f2/07b74f55cd7f9e800b7550d587cbc488f6eaf1 b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/f2/07b74f55cd7f9e800b7550d587cbc488f6eaf1
new file mode 100644
index 0000000000..ae6fdce5a2
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/objects/f2/07b74f55cd7f9e800b7550d587cbc488f6eaf1
Binary files differ
diff --git a/tests/gitea-repositories-meta/migration/lfs-test.git/refs/heads/master b/tests/gitea-repositories-meta/migration/lfs-test.git/refs/heads/master
new file mode 100644
index 0000000000..cd602fb935
--- /dev/null
+++ b/tests/gitea-repositories-meta/migration/lfs-test.git/refs/heads/master
@@ -0,0 +1 @@
+546244003622c64b2fc3c2cd544d7a29882c8383
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMITMESSAGE b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMITMESSAGE
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMITMESSAGE
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMIT_EDITMSG b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMIT_EDITMSG
new file mode 100644
index 0000000000..5852f44639
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMIT_EDITMSG
@@ -0,0 +1 @@
+Initial commit
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/HEAD b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/HEAD
new file mode 100644
index 0000000000..4568acfd1d
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/branch1
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/config b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/config
new file mode 100644
index 0000000000..48ee2884b4
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/config
@@ -0,0 +1,10 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = false
+ logallrefupdates = true
+ symlinks = false
+ ignorecase = true
+[user]
+ name = user2
+ email = user2@example.com
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/config.backup b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/config.backup
new file mode 100644
index 0000000000..d545cdabdb
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/config.backup
@@ -0,0 +1,7 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = false
+ logallrefupdates = true
+ symlinks = false
+ ignorecase = true
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/description b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/index b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/index
new file mode 100644
index 0000000000..450ee4237c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/index
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/HEAD b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/HEAD
new file mode 100644
index 0000000000..19ba979e8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/HEAD
@@ -0,0 +1,2 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491734 +0100 commit (initial): Initial commit
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491742 +0100 checkout: moving from master to branch1
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/branch1 b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/branch1
new file mode 100644
index 0000000000..0501061d19
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/branch1
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491742 +0100 branch: Created from cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/master b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/master
new file mode 100644
index 0000000000..b67741e6b3
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/master
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491734 +0100 commit (initial): Initial commit
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855 b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
new file mode 100644
index 0000000000..fefe858ee7
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
new file mode 100644
index 0000000000..c53ae2e3d6
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
new file mode 100644
index 0000000000..5dce64cf7e
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
@@ -0,0 +1,3 @@
+x•K
+Â0@]ç³$“ß´ âÖcL’)#1oѸzðàñR«µ À࣋@daÊAB$"Ë“ŽI²XcÓ æEæìâä½âmÜ[‡í%ÝÀù‹«¼¹>W9¥V/€ž¼›‘¬ƒ£F­Õn÷Õ¿"u{”Qx…_­>ÿ
+6 \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/branch1 b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/branch1
new file mode 100644
index 0000000000..35f8462fa1
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/branch1
@@ -0,0 +1 @@
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/master b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/master
new file mode 100644
index 0000000000..35f8462fa1
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/master
@@ -0,0 +1 @@
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMITMESSAGE b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMITMESSAGE
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMITMESSAGE
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMIT_EDITMSG b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMIT_EDITMSG
new file mode 100644
index 0000000000..5852f44639
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMIT_EDITMSG
@@ -0,0 +1 @@
+Initial commit
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/HEAD b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/HEAD
new file mode 100644
index 0000000000..4568acfd1d
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/branch1
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config
new file mode 100644
index 0000000000..48ee2884b4
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config
@@ -0,0 +1,10 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = false
+ logallrefupdates = true
+ symlinks = false
+ ignorecase = true
+[user]
+ name = user2
+ email = user2@example.com
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config.backup b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config.backup
new file mode 100644
index 0000000000..d545cdabdb
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config.backup
@@ -0,0 +1,7 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = false
+ logallrefupdates = true
+ symlinks = false
+ ignorecase = true
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/index b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/index
new file mode 100644
index 0000000000..450ee4237c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/index
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/HEAD b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/HEAD
new file mode 100644
index 0000000000..19ba979e8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/HEAD
@@ -0,0 +1,2 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491734 +0100 commit (initial): Initial commit
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491742 +0100 checkout: moving from master to branch1
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/branch1 b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/branch1
new file mode 100644
index 0000000000..0501061d19
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/branch1
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491742 +0100 branch: Created from cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/master b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/master
new file mode 100644
index 0000000000..b67741e6b3
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/master
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491734 +0100 commit (initial): Initial commit
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855 b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
new file mode 100644
index 0000000000..fefe858ee7
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
new file mode 100644
index 0000000000..c53ae2e3d6
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
new file mode 100644
index 0000000000..5dce64cf7e
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
@@ -0,0 +1,3 @@
+x•K
+Â0@]ç³$“ß´ âÖcL’)#1oѸzðàñR«µ À࣋@daÊAB$"Ë“ŽI²XcÓ æEæìâä½âmÜ[‡í%ÝÀù‹«¼¹>W9¥V/€ž¼›‘¬ƒ£F­Õn÷Õ¿"u{”Qx…_­>ÿ
+6 \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/branch1 b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/branch1
new file mode 100644
index 0000000000..35f8462fa1
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/branch1
@@ -0,0 +1 @@
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/master b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/master
new file mode 100644
index 0000000000..35f8462fa1
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/master
@@ -0,0 +1 @@
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMITMESSAGE b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMITMESSAGE
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMITMESSAGE
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMIT_EDITMSG b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMIT_EDITMSG
new file mode 100644
index 0000000000..5852f44639
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMIT_EDITMSG
@@ -0,0 +1 @@
+Initial commit
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/HEAD b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/HEAD
new file mode 100644
index 0000000000..4568acfd1d
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/branch1
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config
new file mode 100644
index 0000000000..48ee2884b4
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config
@@ -0,0 +1,10 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = false
+ logallrefupdates = true
+ symlinks = false
+ ignorecase = true
+[user]
+ name = user2
+ email = user2@example.com
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config.backup b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config.backup
new file mode 100644
index 0000000000..d545cdabdb
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config.backup
@@ -0,0 +1,7 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = false
+ logallrefupdates = true
+ symlinks = false
+ ignorecase = true
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/index b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/index
new file mode 100644
index 0000000000..450ee4237c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/index
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/HEAD b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/HEAD
new file mode 100644
index 0000000000..19ba979e8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/HEAD
@@ -0,0 +1,2 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491734 +0100 commit (initial): Initial commit
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491742 +0100 checkout: moving from master to branch1
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/branch1 b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/branch1
new file mode 100644
index 0000000000..0501061d19
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/branch1
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491742 +0100 branch: Created from cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/master b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/master
new file mode 100644
index 0000000000..b67741e6b3
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/master
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000 cdaca8cf1d36e1e4e508a940f6e157e239beccfa user2 <user2@example.com> 1575491734 +0100 commit (initial): Initial commit
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855 b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
new file mode 100644
index 0000000000..fefe858ee7
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
new file mode 100644
index 0000000000..c53ae2e3d6
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
Binary files differ
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
new file mode 100644
index 0000000000..5dce64cf7e
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
@@ -0,0 +1,3 @@
+x•K
+Â0@]ç³$“ß´ âÖcL’)#1oѸzðàñR«µ À࣋@daÊAB$"Ë“ŽI²XcÓ æEæìâä½âmÜ[‡í%ÝÀù‹«¼¹>W9¥V/€ž¼›‘¬ƒ£F­Õn÷Õ¿"u{”Qx…_­>ÿ
+6 \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/branch1 b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/branch1
new file mode 100644
index 0000000000..35f8462fa1
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/branch1
@@ -0,0 +1 @@
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/master b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/master
new file mode 100644
index 0000000000..35f8462fa1
--- /dev/null
+++ b/tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/master
@@ -0,0 +1 @@
+cdaca8cf1d36e1e4e508a940f6e157e239beccfa
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/HEAD b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/config b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/config
new file mode 100644
index 0000000000..e6da231579
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
+ ignorecase = true
+ precomposeunicode = true
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/description b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/info/exclude b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/6e/75c9f89da9a9b93f4f36e61ed092f7a1625ba0 b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/6e/75c9f89da9a9b93f4f36e61ed092f7a1625ba0
new file mode 100644
index 0000000000..9db794ca36
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/6e/75c9f89da9a9b93f4f36e61ed092f7a1625ba0
Binary files differ
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/7f/eb6f9dd600e17a04f48a76cfa0a56a3f30e2c1 b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/7f/eb6f9dd600e17a04f48a76cfa0a56a3f30e2c1
new file mode 100644
index 0000000000..c219deb1b3
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/7f/eb6f9dd600e17a04f48a76cfa0a56a3f30e2c1
Binary files differ
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/b7/91b41c0ae8cb3c4b12f3fd8c3709c2481d9e37 b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/b7/91b41c0ae8cb3c4b12f3fd8c3709c2481d9e37
new file mode 100644
index 0000000000..60b507e9ca
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/objects/b7/91b41c0ae8cb3c4b12f3fd8c3709c2481d9e37
Binary files differ
diff --git a/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/refs/heads/master b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/refs/heads/master
new file mode 100644
index 0000000000..64e40736d6
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/private_repo_on_private_org.git/refs/heads/master
@@ -0,0 +1 @@
+6e75c9f89da9a9b93f4f36e61ed092f7a1625ba0
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/HEAD b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/config b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/config
new file mode 100644
index 0000000000..e6da231579
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
+ ignorecase = true
+ precomposeunicode = true
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/description b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/info/exclude b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/04/f99c528b643b9175a4b156cdfc13aba6b43853 b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/04/f99c528b643b9175a4b156cdfc13aba6b43853
new file mode 100644
index 0000000000..e97437c599
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/04/f99c528b643b9175a4b156cdfc13aba6b43853
Binary files differ
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/86/de16d8658f5c0a17ec6aa313871295d7072f78 b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/86/de16d8658f5c0a17ec6aa313871295d7072f78
new file mode 100644
index 0000000000..169f196579
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/86/de16d8658f5c0a17ec6aa313871295d7072f78
Binary files differ
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/bf/19fd4707acb403c4aca44f126ab69142ac59ce b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/bf/19fd4707acb403c4aca44f126ab69142ac59ce
new file mode 100644
index 0000000000..d04c6cb3bf
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/objects/bf/19fd4707acb403c4aca44f126ab69142ac59ce
Binary files differ
diff --git a/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/refs/heads/master b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/refs/heads/master
new file mode 100644
index 0000000000..56af246fff
--- /dev/null
+++ b/tests/gitea-repositories-meta/privated_org/public_repo_on_private_org.git/refs/heads/master
@@ -0,0 +1 @@
+bf19fd4707acb403c4aca44f126ab69142ac59ce
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/HEAD b/tests/gitea-repositories-meta/user12/repo10.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/config b/tests/gitea-repositories-meta/user12/repo10.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/description b/tests/gitea-repositories-meta/user12/repo10.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive b/tests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive
new file mode 100755
index 0000000000..4b3d452abc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
+ sh "$SHELL_FOLDER/post-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive b/tests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive
new file mode 100755
index 0000000000..4127013053
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
+ sh "$SHELL_FOLDER/pre-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/hooks/update b/tests/gitea-repositories-meta/user12/repo10.git/hooks/update
new file mode 100755
index 0000000000..c186fe4a18
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/hooks/update
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/update.d"`; do
+ sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user12/repo10.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/info/exclude b/tests/gitea-repositories-meta/user12/repo10.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/info/refs b/tests/gitea-repositories-meta/user12/repo10.git/info/refs
new file mode 100644
index 0000000000..ca1df85e2e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/info/refs
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6 b/tests/gitea-repositories-meta/user12/repo10.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
new file mode 100644
index 0000000000..0994add2c8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
Binary files differ
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f b/tests/gitea-repositories-meta/user12/repo10.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
new file mode 100644
index 0000000000..700a13828e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d b/tests/gitea-repositories-meta/user12/repo10.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
new file mode 100644
index 0000000000..de48ba79f5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/objects/info/packs b/tests/gitea-repositories-meta/user12/repo10.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/DefaultBranch b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/DefaultBranch
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/DefaultBranch
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/develop b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/develop
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/develop
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/feature/1 b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/feature/1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/feature/1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/master b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/master
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/refs/heads/master
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user12/repo10.git/refs/tags/v1.1 b/tests/gitea-repositories-meta/user12/repo10.git/refs/tags/v1.1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user12/repo10.git/refs/tags/v1.1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/HEAD b/tests/gitea-repositories-meta/user13/repo11.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/config b/tests/gitea-repositories-meta/user13/repo11.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/description b/tests/gitea-repositories-meta/user13/repo11.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive b/tests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive
new file mode 100755
index 0000000000..4b3d452abc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
+ sh "$SHELL_FOLDER/post-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive b/tests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive
new file mode 100755
index 0000000000..4127013053
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
+ sh "$SHELL_FOLDER/pre-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/hooks/update b/tests/gitea-repositories-meta/user13/repo11.git/hooks/update
new file mode 100755
index 0000000000..c186fe4a18
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/hooks/update
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/update.d"`; do
+ sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user13/repo11.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/info/exclude b/tests/gitea-repositories-meta/user13/repo11.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/info/refs b/tests/gitea-repositories-meta/user13/repo11.git/info/refs
new file mode 100644
index 0000000000..ca1df85e2e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/info/refs
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/objects/0a/bcb056019adb8336cf9db3ad9d9cf80cd4b141 b/tests/gitea-repositories-meta/user13/repo11.git/objects/0a/bcb056019adb8336cf9db3ad9d9cf80cd4b141
new file mode 100644
index 0000000000..63ba4ed8ee
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/objects/0a/bcb056019adb8336cf9db3ad9d9cf80cd4b141
Binary files differ
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6 b/tests/gitea-repositories-meta/user13/repo11.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
new file mode 100644
index 0000000000..0994add2c8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
Binary files differ
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f b/tests/gitea-repositories-meta/user13/repo11.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
new file mode 100644
index 0000000000..700a13828e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d b/tests/gitea-repositories-meta/user13/repo11.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
new file mode 100644
index 0000000000..de48ba79f5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/objects/75/d1afd00e111c8dbd9e3d96a27b431ac5ae6d74 b/tests/gitea-repositories-meta/user13/repo11.git/objects/75/d1afd00e111c8dbd9e3d96a27b431ac5ae6d74
new file mode 100644
index 0000000000..609b1c0e64
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/objects/75/d1afd00e111c8dbd9e3d96a27b431ac5ae6d74
Binary files differ
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/objects/ed/447543e0c85d628b91f7f466f4921908f4c5ea b/tests/gitea-repositories-meta/user13/repo11.git/objects/ed/447543e0c85d628b91f7f466f4921908f4c5ea
new file mode 100644
index 0000000000..b3a1046fee
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/objects/ed/447543e0c85d628b91f7f466f4921908f4c5ea
Binary files differ
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/objects/info/packs b/tests/gitea-repositories-meta/user13/repo11.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/DefaultBranch b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/DefaultBranch
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/DefaultBranch
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/branch2 b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/branch2
new file mode 100644
index 0000000000..d413449668
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/branch2
@@ -0,0 +1 @@
+0abcb056019adb8336cf9db3ad9d9cf80cd4b141
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/develop b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/develop
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/develop
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/feature/1 b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/feature/1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/feature/1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/master b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/master
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/refs/heads/master
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user13/repo11.git/refs/tags/v1.1 b/tests/gitea-repositories-meta/user13/repo11.git/refs/tags/v1.1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user13/repo11.git/refs/tags/v1.1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/HEAD b/tests/gitea-repositories-meta/user2/commits_search_test.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/config b/tests/gitea-repositories-meta/user2/commits_search_test.git/config
new file mode 100644
index 0000000000..bfbada5989
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/config
@@ -0,0 +1,8 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
+[remote "origin"]
+ url = /home/mura/go/src/code.gitea.io/gitea/tests/gitea-repositories-meta/user2/commits_search_test/
+ fetch = +refs/*:refs/*
+ mirror = true
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/description b/tests/gitea-repositories-meta/user2/commits_search_test.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/info/exclude b/tests/gitea-repositories-meta/user2/commits_search_test.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0a/8499a22ad32a80beda9d75efe15f9f94582468 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0a/8499a22ad32a80beda9d75efe15f9f94582468
new file mode 100644
index 0000000000..c258fcdd3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0a/8499a22ad32a80beda9d75efe15f9f94582468
@@ -0,0 +1,2 @@
+x…ŽÍ
+Â0„=÷)ö.Ê&›Ÿ-ˆè;øq³ÁBcK‰àãÄ»—™aàF–Z§i×6UÈ!)£¥d°çÂ=$§±'ã½crˆ¬ã(4¬iÓg±(©D¾û2æl3fÃÄ1ˆ—ŒƒC%Ò«=– nK…S—‹¾S]g=ÊRÏÐÇç@daŒ8ô¶ßjºÁužDáôµ?Ð:Ðð?‹>9 \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0c/cf1fcd4d1717c22de0707619a5577ea0acebf0 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0c/cf1fcd4d1717c22de0707619a5577ea0acebf0
new file mode 100644
index 0000000000..25b730dc81
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/0c/cf1fcd4d1717c22de0707619a5577ea0acebf0
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3e/a192a6466793d4b7cd8641801ca0c6bec3979c b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3e/a192a6466793d4b7cd8641801ca0c6bec3979c
new file mode 100644
index 0000000000..69b09b5129
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3e/a192a6466793d4b7cd8641801ca0c6bec3979c
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3f/6594f108842b7c50772510e53ce113d3583c4a b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3f/6594f108842b7c50772510e53ce113d3583c4a
new file mode 100644
index 0000000000..18a2af07c8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/3f/6594f108842b7c50772510e53ce113d3583c4a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/58/e97d1a24fb9e1599d8a467ec409430f3d3569e b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/58/e97d1a24fb9e1599d8a467ec409430f3d3569e
new file mode 100644
index 0000000000..585bb66e5b
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/58/e97d1a24fb9e1599d8a467ec409430f3d3569e
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/67/68c1fc1d9448422f05cc84267d94ee62085fe8 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/67/68c1fc1d9448422f05cc84267d94ee62085fe8
new file mode 100644
index 0000000000..2860a7c559
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/67/68c1fc1d9448422f05cc84267d94ee62085fe8
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/6e/8eabd9a7f8d6acd2a1219facfd37415564b144 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/6e/8eabd9a7f8d6acd2a1219facfd37415564b144
new file mode 100644
index 0000000000..4fb1314493
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/6e/8eabd9a7f8d6acd2a1219facfd37415564b144
@@ -0,0 +1,2 @@
+x•[
+1 Eýî*ò/J2é+ ¢KédL© \¾EÜ€_.œ{´Õºv  ‡¾›[!™Jô1&áÅÏI—=e$-¨q6eI¢®¼ú£ípßV5¸|q³w©ÏÍÎÚêu\òÄ'8bFtc©nI?ëDîêç1â \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/85/f46d747a68adf79cc01e2c25ba6a56932d298d b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/85/f46d747a68adf79cc01e2c25ba6a56932d298d
new file mode 100644
index 0000000000..f276d2f694
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/85/f46d747a68adf79cc01e2c25ba6a56932d298d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/8d/dd8d1ad1fdc21ab629e906711fa9bc27aa1c52 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/8d/dd8d1ad1fdc21ab629e906711fa9bc27aa1c52
new file mode 100644
index 0000000000..00c3a450a2
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/8d/dd8d1ad1fdc21ab629e906711fa9bc27aa1c52
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/95/fd0c4138480e4b3913e7cf71a90623fb926fe8 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/95/fd0c4138480e4b3913e7cf71a90623fb926fe8
new file mode 100644
index 0000000000..2dd5fc2dfe
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/95/fd0c4138480e4b3913e7cf71a90623fb926fe8
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/98/00fe78cabf4fe774fcf376f97fa2a0ed06987b b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/98/00fe78cabf4fe774fcf376f97fa2a0ed06987b
new file mode 100644
index 0000000000..42a85f1b77
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/98/00fe78cabf4fe774fcf376f97fa2a0ed06987b
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/9f/cdb7d53bdef786d2e5577948a0c0d4b321fe5a b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/9f/cdb7d53bdef786d2e5577948a0c0d4b321fe5a
new file mode 100644
index 0000000000..125bc6c9e1
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/9f/cdb7d53bdef786d2e5577948a0c0d4b321fe5a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c2/0caf78b5f9dd2d0d183876c5cd0e761c13f7f8 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c2/0caf78b5f9dd2d0d183876c5cd0e761c13f7f8
new file mode 100644
index 0000000000..83e681f9da
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c2/0caf78b5f9dd2d0d183876c5cd0e761c13f7f8
@@ -0,0 +1,2 @@
+xŽK
+1]çÙ‹’ÎoÒ0ˆx“NÒ‚1ÃÁã›…póE¼Ò[{ ˆ‡±3ël¦ÅÇ$”T:–œ³K­°±ÀPeQíü:rbÊi‘T#•j , P‘ê!DŸÁ{Eïq﻾õ¬×9WþPÛž|.½]ôÔÀ#¢³úh’1jÒykðßÁ¯8YõE=Î \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c5/2ba74685f5c8c593efbbb38f62fe024110adef b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c5/2ba74685f5c8c593efbbb38f62fe024110adef
new file mode 100644
index 0000000000..eef09bf6fc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/c5/2ba74685f5c8c593efbbb38f62fe024110adef
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/d6/ae8023a10ff446b6a4e7f441554834008e99c3 b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/d6/ae8023a10ff446b6a4e7f441554834008e99c3
new file mode 100644
index 0000000000..99f8189587
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/objects/d6/ae8023a10ff446b6a4e7f441554834008e99c3
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/packed-refs b/tests/gitea-repositories-meta/user2/commits_search_test.git/packed-refs
new file mode 100644
index 0000000000..7675cfd15f
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/packed-refs
@@ -0,0 +1,2 @@
+# pack-refs with: peeled fully-peeled
+9800fe78cabf4fe774fcf376f97fa2a0ed06987b refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/commits_search_test.git/refs/heads/master b/tests/gitea-repositories-meta/user2/commits_search_test.git/refs/heads/master
new file mode 100644
index 0000000000..81ba7e721a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/commits_search_test.git/refs/heads/master
@@ -0,0 +1 @@
+9800fe78cabf4fe774fcf376f97fa2a0ed06987b
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/HEAD b/tests/gitea-repositories-meta/user2/git_hooks_test.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/config b/tests/gitea-repositories-meta/user2/git_hooks_test.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/description b/tests/gitea-repositories-meta/user2/git_hooks_test.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive
new file mode 100755
index 0000000000..4b3d452abc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
+ sh "$SHELL_FOLDER/post-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive
new file mode 100755
index 0000000000..4127013053
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
+ sh "$SHELL_FOLDER/pre-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive
new file mode 100755
index 0000000000..b26a3b9b68
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/pre-receive.d/pre-receive
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+echo Hello, World!
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update
new file mode 100755
index 0000000000..c186fe4a18
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/update.d"`; do
+ sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/exclude b/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/refs b/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/refs
new file mode 100644
index 0000000000..ca1df85e2e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/info/refs
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6 b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
new file mode 100644
index 0000000000..0994add2c8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
new file mode 100644
index 0000000000..700a13828e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
new file mode 100644
index 0000000000..de48ba79f5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/info/packs b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/DefaultBranch b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/DefaultBranch
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/DefaultBranch
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/develop b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/develop
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/develop
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/feature/1 b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/feature/1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/feature/1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/master b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/master
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/heads/master
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/tags/v1.1 b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/tags/v1.1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/git_hooks_test.git/refs/tags/v1.1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/glob.git/HEAD b/tests/gitea-repositories-meta/user2/glob.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/glob.git/config b/tests/gitea-repositories-meta/user2/glob.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/glob.git/description b/tests/gitea-repositories-meta/user2/glob.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/glob.git/info/exclude b/tests/gitea-repositories-meta/user2/glob.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/48/06cb9df135782b818c968c2fadbd2c150d23d6 b/tests/gitea-repositories-meta/user2/glob.git/objects/48/06cb9df135782b818c968c2fadbd2c150d23d6
new file mode 100644
index 0000000000..a393a43282
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/48/06cb9df135782b818c968c2fadbd2c150d23d6
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/59/fee614e09d1f1cd1e15e4b2a7e9c8873a81498 b/tests/gitea-repositories-meta/user2/glob.git/objects/59/fee614e09d1f1cd1e15e4b2a7e9c8873a81498
new file mode 100644
index 0000000000..a55c8cc6e6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/59/fee614e09d1f1cd1e15e4b2a7e9c8873a81498
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/7c/8ac2f8d82a1eb5f6aaece6629ff11015f91eb4 b/tests/gitea-repositories-meta/user2/glob.git/objects/7c/8ac2f8d82a1eb5f6aaece6629ff11015f91eb4
new file mode 100644
index 0000000000..d5176e68c6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/7c/8ac2f8d82a1eb5f6aaece6629ff11015f91eb4
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/8e/592e636d27ac144f92f7fe8c33631cbdea594d b/tests/gitea-repositories-meta/user2/glob.git/objects/8e/592e636d27ac144f92f7fe8c33631cbdea594d
new file mode 100644
index 0000000000..8034110ebc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/8e/592e636d27ac144f92f7fe8c33631cbdea594d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/95/aff026f99a9ab76fbd01decb63dd3dbc03e498 b/tests/gitea-repositories-meta/user2/glob.git/objects/95/aff026f99a9ab76fbd01decb63dd3dbc03e498
new file mode 100644
index 0000000000..0883f2b3eb
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/95/aff026f99a9ab76fbd01decb63dd3dbc03e498
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/ae/d1ffed24cc3cf9b80490795e893cae4bddd684 b/tests/gitea-repositories-meta/user2/glob.git/objects/ae/d1ffed24cc3cf9b80490795e893cae4bddd684
new file mode 100644
index 0000000000..03fa05de22
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/ae/d1ffed24cc3cf9b80490795e893cae4bddd684
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/bf/d6a6583f9a9ac59bd726c1df26c64a89427ede b/tests/gitea-repositories-meta/user2/glob.git/objects/bf/d6a6583f9a9ac59bd726c1df26c64a89427ede
new file mode 100644
index 0000000000..9475433e66
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/bf/d6a6583f9a9ac59bd726c1df26c64a89427ede
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/c8/eb3b6c767ccb68411d0a1f6c769be69fb4d95a b/tests/gitea-repositories-meta/user2/glob.git/objects/c8/eb3b6c767ccb68411d0a1f6c769be69fb4d95a
new file mode 100644
index 0000000000..2b6297f726
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/c8/eb3b6c767ccb68411d0a1f6c769be69fb4d95a
@@ -0,0 +1 @@
+x+)JMU03d040031QHÒ+©(a¨é:ô㆖ÜÖo«Þ<KšÿQ@ô§Ü P¨dè‹ÔKÎU_#â?éû¿ãd™½¯"}•‡ \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/de/6be43fe8eb19ca3f4e934cb8b9a9a0b20fe865 b/tests/gitea-repositories-meta/user2/glob.git/objects/de/6be43fe8eb19ca3f4e934cb8b9a9a0b20fe865
new file mode 100644
index 0000000000..ece04b3974
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/de/6be43fe8eb19ca3f4e934cb8b9a9a0b20fe865
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/glob.git/objects/ef/6b814b610d8e7717aa0f71fbe5842bcf814697 b/tests/gitea-repositories-meta/user2/glob.git/objects/ef/6b814b610d8e7717aa0f71fbe5842bcf814697
new file mode 100644
index 0000000000..264cf5a196
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/objects/ef/6b814b610d8e7717aa0f71fbe5842bcf814697
@@ -0,0 +1,2 @@
+x­Î9Â0@QjŸb.@ä8“MB*Z®àxÆÁÂŽ#/·'â ´¯øú&†à
+(ÕžJbÍÔZˤИÎØy™$Îrœ{žæÎhÆ…ˆ† …®å<ªóžSˆðLz#—õ'»zßjæ”›-&Þý§Y]yÕ¥11\¡í‡q@D…p–”âÐc£ðß‚âN…së<gñ¿ŽK© \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/glob.git/refs/heads/master b/tests/gitea-repositories-meta/user2/glob.git/refs/heads/master
new file mode 100644
index 0000000000..bca1628455
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/glob.git/refs/heads/master
@@ -0,0 +1 @@
+ef6b814b610d8e7717aa0f71fbe5842bcf814697
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/HEAD b/tests/gitea-repositories-meta/user2/repo1.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/config b/tests/gitea-repositories-meta/user2/repo1.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/description b/tests/gitea-repositories-meta/user2/repo1.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive b/tests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive
new file mode 100755
index 0000000000..4b3d452abc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
+ sh "$SHELL_FOLDER/post-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive b/tests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive
new file mode 100755
index 0000000000..4127013053
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
+ sh "$SHELL_FOLDER/pre-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/hooks/update b/tests/gitea-repositories-meta/user2/repo1.git/hooks/update
new file mode 100755
index 0000000000..c186fe4a18
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/hooks/update
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/update.d"`; do
+ sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user2/repo1.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/info/exclude b/tests/gitea-repositories-meta/user2/repo1.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/info/refs b/tests/gitea-repositories-meta/user2/repo1.git/info/refs
new file mode 100644
index 0000000000..fa3009793d
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/info/refs
@@ -0,0 +1,3 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master
+985f0301dba5e7b34be866819cd15ad3d8f508ee refs/heads/branch2
+62fb502a7172d4453f0322a2cc85bddffa57f07a refs/heads/pr-to-update
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/00/750edc07d6415dcc07ae0351e9397b0222b7ba b/tests/gitea-repositories-meta/user2/repo1.git/objects/00/750edc07d6415dcc07ae0351e9397b0222b7ba
new file mode 100644
index 0000000000..d3c45d51ea
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/00/750edc07d6415dcc07ae0351e9397b0222b7ba
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6 b/tests/gitea-repositories-meta/user2/repo1.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
new file mode 100644
index 0000000000..0994add2c8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/2a/2f1d4670728a2e10049e345bd7a276468beab6
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/3f/a2f829675543ecfc16b2891aebe8bf0608a8f4 b/tests/gitea-repositories-meta/user2/repo1.git/objects/3f/a2f829675543ecfc16b2891aebe8bf0608a8f4
new file mode 100644
index 0000000000..892c6bffe9
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/3f/a2f829675543ecfc16b2891aebe8bf0608a8f4
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/4a/357436d925b5c974181ff12a994538ddc5a269 b/tests/gitea-repositories-meta/user2/repo1.git/objects/4a/357436d925b5c974181ff12a994538ddc5a269
new file mode 100644
index 0000000000..bf97d00fd8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/4a/357436d925b5c974181ff12a994538ddc5a269
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f b/tests/gitea-repositories-meta/user2/repo1.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
new file mode 100644
index 0000000000..700a13828e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/4b/4851ad51df6a7d9f25c979345979eaeb5b349f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 b/tests/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2
new file mode 100644
index 0000000000..c0cb626359
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/5f/22f7d0d95d614d25a5b68592adb345a4b5c7fd b/tests/gitea-repositories-meta/user2/repo1.git/objects/5f/22f7d0d95d614d25a5b68592adb345a4b5c7fd
new file mode 100644
index 0000000000..17fdf18edb
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/5f/22f7d0d95d614d25a5b68592adb345a4b5c7fd
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a b/tests/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a
new file mode 100644
index 0000000000..ee494a8ca8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d b/tests/gitea-repositories-meta/user2/repo1.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
new file mode 100644
index 0000000000..de48ba79f5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/65/f1bf27bc3bf70f64657658635e66094edbcb4d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb b/tests/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb
new file mode 100644
index 0000000000..09aed946f2
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 b/tests/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804
new file mode 100644
index 0000000000..3bf67a206a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/90/dcd07da077d1e7cd6032b52d1f79ae2b5f19b2 b/tests/gitea-repositories-meta/user2/repo1.git/objects/90/dcd07da077d1e7cd6032b52d1f79ae2b5f19b2
new file mode 100644
index 0000000000..1404abdaa2
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/90/dcd07da077d1e7cd6032b52d1f79ae2b5f19b2
@@ -0,0 +1,2 @@
+xe±NÄ0D©#åæŽ4
+JÄAÅ5”Ž³—,—xÑzsVþ‚5„DåÑØ»ž7ý,=®®o.áEå¢áq5J=éˆýÈ rÄ=>4§ú O!óŠýã´ðÐ6ms˜8ƒ¾&\Ea¾tÍT„´I¢z”‰Ô…! ¢dso@a›Ú&ÌK5üB)›r4–”Q¦`YèLÚ¯²b ›¾o`Ûaä3¹@(ÒeýÔ­5 ôÂH—\sÔHÿ9Ÿ9Rª3)Îë@ŽSùã_"§‘4sE0”Rºñ§¤.‘U|/€m¦Û¿]U÷ÌzÀ \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 b/tests/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9
new file mode 100644
index 0000000000..60692df6ec
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee b/tests/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee
new file mode 100644
index 0000000000..81fd6a50fd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a b/tests/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a
new file mode 100644
index 0000000000..887669883b
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d b/tests/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d
new file mode 100644
index 0000000000..c3111a08b8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd b/tests/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd
new file mode 100644
index 0000000000..9182ac0381
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/d4/a1a6dcf7bd42891f264d484e80dac7e66b5410 b/tests/gitea-repositories-meta/user2/repo1.git/objects/d4/a1a6dcf7bd42891f264d484e80dac7e66b5410
new file mode 100644
index 0000000000..d7ef93c616
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/d4/a1a6dcf7bd42891f264d484e80dac7e66b5410
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/d7/bd5b8cfb680f460e37b6fd7cf74c284e059118 b/tests/gitea-repositories-meta/user2/repo1.git/objects/d7/bd5b8cfb680f460e37b6fd7cf74c284e059118
new file mode 100644
index 0000000000..6039ff6619
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/d7/bd5b8cfb680f460e37b6fd7cf74c284e059118
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/dc/7a8ba127fee870dd683310ce660dfe59333a1b b/tests/gitea-repositories-meta/user2/repo1.git/objects/dc/7a8ba127fee870dd683310ce660dfe59333a1b
new file mode 100644
index 0000000000..7678d6754d
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/dc/7a8ba127fee870dd683310ce660dfe59333a1b
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/dd/59742c0f6672911f2b64cba5711ac00593ed32 b/tests/gitea-repositories-meta/user2/repo1.git/objects/dd/59742c0f6672911f2b64cba5711ac00593ed32
new file mode 100644
index 0000000000..f9137c5f77
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/dd/59742c0f6672911f2b64cba5711ac00593ed32
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/objects/info/packs b/tests/gitea-repositories-meta/user2/repo1.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/DefaultBranch b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/DefaultBranch
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/DefaultBranch
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2 b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2
new file mode 100644
index 0000000000..5add7256cd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2
@@ -0,0 +1 @@
+985f0301dba5e7b34be866819cd15ad3d8f508ee
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/develop b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/develop
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/develop
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/feature/1 b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/feature/1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/feature/1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/master
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/master
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update
new file mode 100644
index 0000000000..e0ee44dd14
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update
@@ -0,0 +1 @@
+62fb502a7172d4453f0322a2cc85bddffa57f07a
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/notes/commits b/tests/gitea-repositories-meta/user2/repo1.git/refs/notes/commits
new file mode 100644
index 0000000000..6f837536fc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/notes/commits
@@ -0,0 +1 @@
+3fa2f829675543ecfc16b2891aebe8bf0608a8f4
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head
new file mode 100644
index 0000000000..98593d6537
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head
@@ -0,0 +1 @@
+4a357436d925b5c974181ff12a994538ddc5a269
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head
new file mode 100644
index 0000000000..33a9eaa7f1
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/3/head
@@ -0,0 +1 @@
+5f22f7d0d95d614d25a5b68592adb345a4b5c7fd
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head
new file mode 100644
index 0000000000..e0ee44dd14
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head
@@ -0,0 +1 @@
+62fb502a7172d4453f0322a2cc85bddffa57f07a
diff --git a/tests/gitea-repositories-meta/user2/repo1.git/refs/tags/v1.1 b/tests/gitea-repositories-meta/user2/repo1.git/refs/tags/v1.1
new file mode 100644
index 0000000000..f98a263be6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.git/refs/tags/v1.1
@@ -0,0 +1 @@
+65f1bf27bc3bf70f64657658635e66094edbcb4d
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/HEAD b/tests/gitea-repositories-meta/user2/repo1.wiki.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/config b/tests/gitea-repositories-meta/user2/repo1.wiki.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/description b/tests/gitea-repositories-meta/user2/repo1.wiki.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/info/exclude b/tests/gitea-repositories-meta/user2/repo1.wiki.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec3 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec3
new file mode 100644
index 0000000000..c0314c5584
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec3
@@ -0,0 +1,2 @@
+x­ŽÑmÄ DóMÛÀY¬Í¢(ùJ©`5ÇÉœ-›K*Ki,Hi!?£Ñ<éiâVki0ZÿÔXH“D(Z6ĨGòSb» 3“JDÞhµó!÷uB¬ÌDaJp¡ íœÙèFôLƹ4+~´ëvÀ;‡£È
+eýäžõç[Nx>KÝäÎü‡_så²q«/€]09MHpѤµêk¿Üä_dê-%¸í’‡Ûžï vÎ_¥]¡Ô^Õ/èI[t \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0d/ca5bd9b5d7ef937710e056f575e86c0184ba85 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0d/ca5bd9b5d7ef937710e056f575e86c0184ba85
new file mode 100644
index 0000000000..a46c1925ec
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/0d/ca5bd9b5d7ef937710e056f575e86c0184ba85
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/2c/54faec6c45d31c1abfaecdab471eac6633738a b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/2c/54faec6c45d31c1abfaecdab471eac6633738a
new file mode 100644
index 0000000000..4cf6cdaa6f
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/2c/54faec6c45d31c1abfaecdab471eac6633738a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3
new file mode 100644
index 0000000000..d52aa8e1ff
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196
new file mode 100644
index 0000000000..bf4ae859f6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc
new file mode 100644
index 0000000000..84ade81980
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc
@@ -0,0 +1 @@
+xÆM‚@ †á¯MÛ àºré›°6ñœ&&&¬ü9LežÅ›w½Ý×åt<#ÞñÃÍ¡ªmv-·•0w¬b¦¢jyÌ–†¤Ú—~Ý‹[žæÉçý=HÄ ÷.¾"à‚íµÄçÇ= \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20
new file mode 100644
index 0000000000..052fdf35a5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45e b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45e
new file mode 100644
index 0000000000..bcb0e0075c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45e
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7
new file mode 100644
index 0000000000..9c26495605
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/89/43a1d5f93c00439d5ffc0f8e36f5d60abae46c b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/89/43a1d5f93c00439d5ffc0f8e36f5d60abae46c
new file mode 100644
index 0000000000..062641bee7
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/89/43a1d5f93c00439d5ffc0f8e36f5d60abae46c
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d6 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d6
new file mode 100644
index 0000000000..8a6345dfa5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d6
@@ -0,0 +1,2 @@
+x­ŽÝmÃ0ƒû¬)nú±t2íSèçÓÙ`ņ¥¶“e‚,VY¡/Hâ#È[)¹EûÒ@NÈq¦è툎Ñr2«)DöÅ0âŒj§C®ìÑLŸœaCÓÃ&š4Bv]$Eßí²ðIÓ‘e…¯¼þP×r¿I…sÍe“zªË³~_
+åõÄ[yã‡è¢v£WíµV=í—›ü˘úH vZ~s»@݉%Á•Š¨?TÊZH \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64
new file mode 100644
index 0000000000..6dcfc96676
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e5/3d079e581fbfdea1075a54d5b621eab0090e52 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e5/3d079e581fbfdea1075a54d5b621eab0090e52
new file mode 100644
index 0000000000..ecdea7f694
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e5/3d079e581fbfdea1075a54d5b621eab0090e52
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/ea/82fc8777a24b07c26b3a4bf4e2742c03733eab b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/ea/82fc8777a24b07c26b3a4bf4e2742c03733eab
new file mode 100644
index 0000000000..42a8258dbd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/ea/82fc8777a24b07c26b3a4bf4e2742c03733eab
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940
new file mode 100644
index 0000000000..eaeadaeaee
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
new file mode 100644
index 0000000000..38984b12b7
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master
@@ -0,0 +1 @@
+0dca5bd9b5d7ef937710e056f575e86c0184ba85
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/HEAD b/tests/gitea-repositories-meta/user2/repo15.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/config b/tests/gitea-repositories-meta/user2/repo15.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/description b/tests/gitea-repositories-meta/user2/repo15.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive b/tests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive
new file mode 100755
index 0000000000..4b3d452abc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
+ sh "$SHELL_FOLDER/post-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive b/tests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive
new file mode 100755
index 0000000000..4127013053
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
+ sh "$SHELL_FOLDER/pre-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/hooks/update b/tests/gitea-repositories-meta/user2/repo15.git/hooks/update
new file mode 100755
index 0000000000..c186fe4a18
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/hooks/update
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/update.d"`; do
+ sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user2/repo15.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user2/repo15.git/info/exclude b/tests/gitea-repositories-meta/user2/repo15.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo15.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/HEAD b/tests/gitea-repositories-meta/user2/repo16.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/config b/tests/gitea-repositories-meta/user2/repo16.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/description b/tests/gitea-repositories-meta/user2/repo16.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/info/exclude b/tests/gitea-repositories-meta/user2/repo16.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/0c/3d59dea27b97aa3cb66072745d7a2c51a7a8b1 b/tests/gitea-repositories-meta/user2/repo16.git/objects/0c/3d59dea27b97aa3cb66072745d7a2c51a7a8b1
new file mode 100644
index 0000000000..e62f09ac5f
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/0c/3d59dea27b97aa3cb66072745d7a2c51a7a8b1
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/24/f83a471f77579fea57bac7255d6e64e70fce1c b/tests/gitea-repositories-meta/user2/repo16.git/objects/24/f83a471f77579fea57bac7255d6e64e70fce1c
new file mode 100644
index 0000000000..2558be67ee
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/24/f83a471f77579fea57bac7255d6e64e70fce1c
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/27/566bd5738fc8b4e3fef3c5e72cce608537bd95 b/tests/gitea-repositories-meta/user2/repo16.git/objects/27/566bd5738fc8b4e3fef3c5e72cce608537bd95
new file mode 100644
index 0000000000..6042481f4e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/27/566bd5738fc8b4e3fef3c5e72cce608537bd95
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/3b/2b54fe3d9a8279d5b926124dccdf279b8eff2f b/tests/gitea-repositories-meta/user2/repo16.git/objects/3b/2b54fe3d9a8279d5b926124dccdf279b8eff2f
new file mode 100644
index 0000000000..13de5951f1
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/3b/2b54fe3d9a8279d5b926124dccdf279b8eff2f
@@ -0,0 +1 @@
+x+)JMU06g040031Q(JMLÉMÕËMaXbR¶”10-&ªéCüÕþË’‘Ý=À, \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/45/8121ce9a6b855c9733bae62093caf3f39685de b/tests/gitea-repositories-meta/user2/repo16.git/objects/45/8121ce9a6b855c9733bae62093caf3f39685de
new file mode 100644
index 0000000000..7db2d33de1
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/45/8121ce9a6b855c9733bae62093caf3f39685de
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/50/99b81332712fe655e34e8dd63574f503f61811 b/tests/gitea-repositories-meta/user2/repo16.git/objects/50/99b81332712fe655e34e8dd63574f503f61811
new file mode 100644
index 0000000000..30997633f6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/50/99b81332712fe655e34e8dd63574f503f61811
@@ -0,0 +1,2 @@
+xQ
+B!Eûvó„ŽO'!¢M´€Q‡×ƒÌðù ågÔúº‡ çÞTKY:v½‰€N6»…‘b f›¢÷š&—‰19ÃÄÇhoýV\Wi§íyqyÞåj9ƒqõ„FØÒj´ãªË?ÙŸ¤æZ3¬Ëü ßõ*S6# \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/69/554a64c1e6030f051e5c3f94bfbd773cd6a324 b/tests/gitea-repositories-meta/user2/repo16.git/objects/69/554a64c1e6030f051e5c3f94bfbd773cd6a324
new file mode 100644
index 0000000000..77ea95d297
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/69/554a64c1e6030f051e5c3f94bfbd773cd6a324
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/a4/3476a501516e065c5a82f05fd58fd319598bc1 b/tests/gitea-repositories-meta/user2/repo16.git/objects/a4/3476a501516e065c5a82f05fd58fd319598bc1
new file mode 100644
index 0000000000..e021b19eff
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/a4/3476a501516e065c5a82f05fd58fd319598bc1
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/e9/4083fcdf1f10c545e9253a23c5e44a2ff68aac b/tests/gitea-repositories-meta/user2/repo16.git/objects/e9/4083fcdf1f10c545e9253a23c5e44a2ff68aac
new file mode 100644
index 0000000000..780affc887
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/e9/4083fcdf1f10c545e9253a23c5e44a2ff68aac
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/f2/7c2b2b03dcab38beaf89b0ab4ff61f6de63441 b/tests/gitea-repositories-meta/user2/repo16.git/objects/f2/7c2b2b03dcab38beaf89b0ab4ff61f6de63441
new file mode 100644
index 0000000000..7cb8b91898
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/f2/7c2b2b03dcab38beaf89b0ab4ff61f6de63441
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/objects/f9/0451c72ef61a7645293d17b47be7a8e983da57 b/tests/gitea-repositories-meta/user2/repo16.git/objects/f9/0451c72ef61a7645293d17b47be7a8e983da57
new file mode 100644
index 0000000000..c7627ad36a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/objects/f9/0451c72ef61a7645293d17b47be7a8e983da57
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign
new file mode 100644
index 0000000000..4750a76432
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign
@@ -0,0 +1 @@
+f27c2b2b03dcab38beaf89b0ab4ff61f6de63441
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign-not-yet-validated b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign-not-yet-validated
new file mode 100644
index 0000000000..f68025f5b8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/good-sign-not-yet-validated
@@ -0,0 +1 @@
+27566bd5738fc8b4e3fef3c5e72cce608537bd95
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/master
new file mode 100644
index 0000000000..65f9a9f714
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/master
@@ -0,0 +1 @@
+69554a64c1e6030f051e5c3f94bfbd773cd6a324
diff --git a/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/not-signed b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/not-signed
new file mode 100644
index 0000000000..65f9a9f714
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo16.git/refs/heads/not-signed
@@ -0,0 +1 @@
+69554a64c1e6030f051e5c3f94bfbd773cd6a324
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/HEAD b/tests/gitea-repositories-meta/user2/repo2.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/config b/tests/gitea-repositories-meta/user2/repo2.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/description b/tests/gitea-repositories-meta/user2/repo2.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/info/exclude b/tests/gitea-repositories-meta/user2/repo2.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/info/refs b/tests/gitea-repositories-meta/user2/repo2.git/info/refs
new file mode 100644
index 0000000000..044e52e0f9
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/info/refs
@@ -0,0 +1 @@
+205ac761f3326a7ebe416e8673760016450b5cec refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/0a/7d8b41ae9763e9a1743917396839d1791d49d0 b/tests/gitea-repositories-meta/user2/repo2.git/objects/0a/7d8b41ae9763e9a1743917396839d1791d49d0
new file mode 100644
index 0000000000..d62e3c623e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/0a/7d8b41ae9763e9a1743917396839d1791d49d0
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec3 b/tests/gitea-repositories-meta/user2/repo2.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec3
new file mode 100644
index 0000000000..c0314c5584
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/0c/f15c3f66ec8384480ed9c3cf87c9e97fbb0ec3
@@ -0,0 +1,2 @@
+x­ŽÑmÄ DóMÛÀY¬Í¢(ùJ©`5ÇÉœ-›K*Ki,Hi!?£Ñ<éiâVki0ZÿÔXH“D(Z6ĨGòSb» 3“JDÞhµó!÷uB¬ÌDaJp¡ íœÙèFôLƹ4+~´ëvÀ;‡£È
+eýäžõç[Nx>KÝäÎü‡_så²q«/€]09MHpѤµêk¿Üä_dê-%¸í’‡Ûžï vÎ_¥]¡Ô^Õ/èI[t \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c99278700 b/tests/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c99278700
new file mode 100644
index 0000000000..736e40878e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/10/32bbf17fbc0d9c95bb5418dabe8f8c99278700
@@ -0,0 +1,2 @@
+xŽK
+Â0Eg™ %Ÿ×":uä’ôŠ•¦J|‚îÞê¸çpË­ÖQ´ó~% Ð9Ù„à‘G6GÎ ”ͦw(êžæE4}*íÙ{Ç)`YƆƒlŒeêMî—„JO¹Üš>Žµ¾õ©%ˆÞ^¿ÐÝ¿°L˜!]ÆN[v#E½6ÎU~/ÿúê0 ZðîU'õgpJ5 \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/1c/887eaa8d81fa86da7695d8f635cf17813eb422 b/tests/gitea-repositories-meta/user2/repo2.git/objects/1c/887eaa8d81fa86da7695d8f635cf17813eb422
new file mode 100644
index 0000000000..34fa593277
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/1c/887eaa8d81fa86da7695d8f635cf17813eb422
@@ -0,0 +1 @@
+x+)JMU07b040031QÈ*HM×Ë*Hg(œ(ý¥=í¸„¨ÄAvNAÆù»6þªÉÉÌKÕ+.KgHžº­OþÝn9ŸÔjÿùÙ‹Ò³4l¸é \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/26/f842bcad37fa40a1bb34cbb5ee219ee35d863d b/tests/gitea-repositories-meta/user2/repo2.git/objects/26/f842bcad37fa40a1bb34cbb5ee219ee35d863d
new file mode 100644
index 0000000000..c3e7e778c5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/26/f842bcad37fa40a1bb34cbb5ee219ee35d863d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3 b/tests/gitea-repositories-meta/user2/repo2.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3
new file mode 100644
index 0000000000..d52aa8e1ff
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/32/5dc4f8e9344e6668f21536a69d5f1d4ed53ca3
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/36/fff01c8c9f722d49d53186abd27b5be8d85338 b/tests/gitea-repositories-meta/user2/repo2.git/objects/36/fff01c8c9f722d49d53186abd27b5be8d85338
new file mode 100644
index 0000000000..fc0c8654b5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/36/fff01c8c9f722d49d53186abd27b5be8d85338
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196 b/tests/gitea-repositories-meta/user2/repo2.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196
new file mode 100644
index 0000000000..bf4ae859f6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/42/3313fbd38093bb10d0c8387db9105409c6f196
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc b/tests/gitea-repositories-meta/user2/repo2.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc
new file mode 100644
index 0000000000..84ade81980
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/71/911bf48766c7181518c1070911019fbb00b1fc
@@ -0,0 +1 @@
+xÆM‚@ †á¯MÛ àºré›°6ñœ&&&¬ü9LežÅ›w½Ý×åt<#ÞñÃÍ¡ªmv-·•0w¬b¦¢jyÌ–†¤Ú—~Ý‹[žæÉçý=HÄ ÷.¾"à‚íµÄçÇ= \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20 b/tests/gitea-repositories-meta/user2/repo2.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20
new file mode 100644
index 0000000000..052fdf35a5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/72/fc6251cc648e914c10009d31431fa2e38b9a20
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45e b/tests/gitea-repositories-meta/user2/repo2.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45e
new file mode 100644
index 0000000000..bcb0e0075c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/74/d5a0d73db9b9ef7aa9978eb7a099b08f54d45e
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7 b/tests/gitea-repositories-meta/user2/repo2.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7
new file mode 100644
index 0000000000..9c26495605
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/7c/d7c8fa852973c72c66eb120a6677c54a8697f7
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/ba/1aed4e2ea2443d76cec241b96be4ec990852ec b/tests/gitea-repositories-meta/user2/repo2.git/objects/ba/1aed4e2ea2443d76cec241b96be4ec990852ec
new file mode 100644
index 0000000000..add9a3af0d
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/ba/1aed4e2ea2443d76cec241b96be4ec990852ec
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d6 b/tests/gitea-repositories-meta/user2/repo2.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d6
new file mode 100644
index 0000000000..8a6345dfa5
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/c1/0d10b7e655b3dab1f53176db57c8219a5488d6
@@ -0,0 +1,2 @@
+x­ŽÝmÃ0ƒû¬)nú±t2íSèçÓÙ`ņ¥¶“e‚,VY¡/Hâ#È[)¹EûÒ@NÈq¦è툎Ñr2«)DöÅ0âŒj§C®ìÑLŸœaCÓÃ&š4Bv]$Eßí²ðIÓ‘e…¯¼þP×r¿I…sÍe“zªË³~_
+åõÄ[yã‡è¢v£WíµV=í—›ü˘úH vZ~s»@݉%Á•Š¨?TÊZH \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64 b/tests/gitea-repositories-meta/user2/repo2.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64
new file mode 100644
index 0000000000..6dcfc96676
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/c4/b38c3e1395393f75bbbc2ed10c7eeb577d3b64
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940 b/tests/gitea-repositories-meta/user2/repo2.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940
new file mode 100644
index 0000000000..eaeadaeaee
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/f5/05ec9b5c7a45a10259c1dda7f18434e5d55940
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/info/commit-graph b/tests/gitea-repositories-meta/user2/repo2.git/objects/info/commit-graph
new file mode 100644
index 0000000000..67dae50e83
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/info/commit-graph
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/info/packs b/tests/gitea-repositories-meta/user2/repo2.git/objects/info/packs
new file mode 100644
index 0000000000..9eb91c8e0e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/info/packs
@@ -0,0 +1,2 @@
+P pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.pack
+
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.bitmap b/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.bitmap
new file mode 100644
index 0000000000..8ecce324f4
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.bitmap
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.idx b/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.idx
new file mode 100644
index 0000000000..c4f319811e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.idx
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.pack b/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.pack
new file mode 100644
index 0000000000..9d10156ca7
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/objects/pack/pack-a2f7ad943b3d857eb3ebdb4b35eeef38f63cf5d2.pack
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/packed-refs b/tests/gitea-repositories-meta/user2/repo2.git/packed-refs
new file mode 100644
index 0000000000..f785d91022
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/packed-refs
@@ -0,0 +1,2 @@
+# pack-refs with: peeled fully-peeled sorted
+205ac761f3326a7ebe416e8673760016450b5cec refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo2.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo2.git/refs/heads/master
new file mode 100644
index 0000000000..334d09ca02
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo2.git/refs/heads/master
@@ -0,0 +1 @@
+1032bbf17fbc0d9c95bb5418dabe8f8c99278700
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/HEAD b/tests/gitea-repositories-meta/user2/repo20.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/config b/tests/gitea-repositories-meta/user2/repo20.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/description b/tests/gitea-repositories-meta/user2/repo20.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive b/tests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive
new file mode 100755
index 0000000000..f1f2709ddd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+data=$(cat)
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+echo "${data}" | "${hook}"
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..ee2ab2f2df
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"/home/tris/Projects/go/src/code.gitea.io/gitea/gitea" hook --config='/home/tris/Projects/go/src/code.gitea.io/gitea/custom/conf/app.ini' post-receive
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive b/tests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive
new file mode 100755
index 0000000000..f1f2709ddd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+data=$(cat)
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+echo "${data}" | "${hook}"
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..cdbc3c7c1a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"/home/tris/Projects/go/src/code.gitea.io/gitea/gitea" hook --config='/home/tris/Projects/go/src/code.gitea.io/gitea/custom/conf/app.ini' pre-receive
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/hooks/update b/tests/gitea-repositories-meta/user2/repo20.git/hooks/update
new file mode 100755
index 0000000000..df5bd27f10
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/hooks/update
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+"${hook}" $1 $2 $3
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user2/repo20.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..7447b2fe01
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"/home/tris/Projects/go/src/code.gitea.io/gitea/gitea" hook --config='/home/tris/Projects/go/src/code.gitea.io/gitea/custom/conf/app.ini' update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/info/exclude b/tests/gitea-repositories-meta/user2/repo20.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/info/refs b/tests/gitea-repositories-meta/user2/repo20.git/info/refs
new file mode 100644
index 0000000000..6d83c82bcf
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/info/refs
@@ -0,0 +1 @@
+808038d2f71b0ab020991439cffd24309c7bc530 refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/02/15cbe13d2695a2c3464ab5e59f47f37c3ff5d5 b/tests/gitea-repositories-meta/user2/repo20.git/objects/02/15cbe13d2695a2c3464ab5e59f47f37c3ff5d5
new file mode 100644
index 0000000000..17868e9d33
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/02/15cbe13d2695a2c3464ab5e59f47f37c3ff5d5
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/05/81d7edf45206787ff93956ea892e8a2ae77604 b/tests/gitea-repositories-meta/user2/repo20.git/objects/05/81d7edf45206787ff93956ea892e8a2ae77604
new file mode 100644
index 0000000000..10ab94aa99
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/05/81d7edf45206787ff93956ea892e8a2ae77604
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/1b/271d83842d348b1ee71d8e6ead400aaeb4d1b5 b/tests/gitea-repositories-meta/user2/repo20.git/objects/1b/271d83842d348b1ee71d8e6ead400aaeb4d1b5
new file mode 100644
index 0000000000..01b07ff6e7
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/1b/271d83842d348b1ee71d8e6ead400aaeb4d1b5
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/29/5ba6ac57fdd46f62a51272f40e60b6dea697b2 b/tests/gitea-repositories-meta/user2/repo20.git/objects/29/5ba6ac57fdd46f62a51272f40e60b6dea697b2
new file mode 100644
index 0000000000..8a24f2e7b2
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/29/5ba6ac57fdd46f62a51272f40e60b6dea697b2
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/2c/ec0f7069ed09d934e904c49f414d8bdf818ce4 b/tests/gitea-repositories-meta/user2/repo20.git/objects/2c/ec0f7069ed09d934e904c49f414d8bdf818ce4
new file mode 100644
index 0000000000..c113af8cde
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/2c/ec0f7069ed09d934e904c49f414d8bdf818ce4
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/41/4a282859758ba7b159bfbd9c2b193eb8f135ee b/tests/gitea-repositories-meta/user2/repo20.git/objects/41/4a282859758ba7b159bfbd9c2b193eb8f135ee
new file mode 100644
index 0000000000..c6fb0cfe68
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/41/4a282859758ba7b159bfbd9c2b193eb8f135ee
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/80/8038d2f71b0ab020991439cffd24309c7bc530 b/tests/gitea-repositories-meta/user2/repo20.git/objects/80/8038d2f71b0ab020991439cffd24309c7bc530
new file mode 100644
index 0000000000..21147c874c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/80/8038d2f71b0ab020991439cffd24309c7bc530
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/83/70977f63979e140b6b58992b1fdb4098b24cd9 b/tests/gitea-repositories-meta/user2/repo20.git/objects/83/70977f63979e140b6b58992b1fdb4098b24cd9
new file mode 100644
index 0000000000..3a20da80ba
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/83/70977f63979e140b6b58992b1fdb4098b24cd9
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/8c/e1dee41e1a3700819a9a309f275f8dc7b7e0b6 b/tests/gitea-repositories-meta/user2/repo20.git/objects/8c/e1dee41e1a3700819a9a309f275f8dc7b7e0b6
new file mode 100644
index 0000000000..fa58c0319b
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/8c/e1dee41e1a3700819a9a309f275f8dc7b7e0b6
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/ba/3aeafe10402c6b29535a58d91def7e43638d9d b/tests/gitea-repositories-meta/user2/repo20.git/objects/ba/3aeafe10402c6b29535a58d91def7e43638d9d
new file mode 100644
index 0000000000..eeb034d2f4
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/ba/3aeafe10402c6b29535a58d91def7e43638d9d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/c5/0ac6b9e25abb8200bb377755367d7265c581cf b/tests/gitea-repositories-meta/user2/repo20.git/objects/c5/0ac6b9e25abb8200bb377755367d7265c581cf
new file mode 100644
index 0000000000..7b03dcc620
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/c5/0ac6b9e25abb8200bb377755367d7265c581cf
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a b/tests/gitea-repositories-meta/user2/repo20.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
new file mode 100644
index 0000000000..6802d49492
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/db/89c972fc57862eae378f45b74aca228037d415 b/tests/gitea-repositories-meta/user2/repo20.git/objects/db/89c972fc57862eae378f45b74aca228037d415
new file mode 100644
index 0000000000..c627859fd0
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/db/89c972fc57862eae378f45b74aca228037d415
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/objects/info/packs b/tests/gitea-repositories-meta/user2/repo20.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user2/repo20.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo20.git/refs/heads/master
new file mode 100644
index 0000000000..66b845cd38
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/repo20.git/refs/heads/master
@@ -0,0 +1 @@
+808038d2f71b0ab020991439cffd24309c7bc530
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/HEAD b/tests/gitea-repositories-meta/user2/utf8.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/config b/tests/gitea-repositories-meta/user2/utf8.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/description b/tests/gitea-repositories-meta/user2/utf8.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive b/tests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive
new file mode 100755
index 0000000000..4b3d452abc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
+ sh "$SHELL_FOLDER/post-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive b/tests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive
new file mode 100755
index 0000000000..4127013053
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
+ sh "$SHELL_FOLDER/pre-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/hooks/update b/tests/gitea-repositories-meta/user2/utf8.git/hooks/update
new file mode 100755
index 0000000000..c186fe4a18
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/hooks/update
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/update.d"`; do
+ sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user2/utf8.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/info/exclude b/tests/gitea-repositories-meta/user2/utf8.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/info/refs b/tests/gitea-repositories-meta/user2/utf8.git/info/refs
new file mode 100644
index 0000000000..29eaf9f16c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/info/refs
@@ -0,0 +1,9 @@
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5 refs/heads/Grüßen
+3a810dbf6b96afaa8c5f69a8b6ec1dabfca7368b refs/heads/Plus+Is+Not+Space
+3aa73c3499bff049a352b4e265575373e964b89a refs/heads/master
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5 refs/heads/ГлавнаÑВетка
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5 refs/heads/а/б/в
+28d579e4920fbf4f66e71dab3e779d9fbf41422a refs/heads/ブランãƒ
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5 refs/tags/Ð/人
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5 refs/tags/ТÑг
+28d579e4920fbf4f66e71dab3e779d9fbf41422a refs/tags/ã‚¿ã‚°
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/14/c42687126acae9d1ad41d7bdb528f811065a6a b/tests/gitea-repositories-meta/user2/utf8.git/objects/14/c42687126acae9d1ad41d7bdb528f811065a6a
new file mode 100644
index 0000000000..19fdbf1232
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/14/c42687126acae9d1ad41d7bdb528f811065a6a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/1d/5e00f305a7ca6a8a94e65456820a6d260adab8 b/tests/gitea-repositories-meta/user2/utf8.git/objects/1d/5e00f305a7ca6a8a94e65456820a6d260adab8
new file mode 100644
index 0000000000..684b457c3e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/1d/5e00f305a7ca6a8a94e65456820a6d260adab8
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/28/d579e4920fbf4f66e71dab3e779d9fbf41422a b/tests/gitea-repositories-meta/user2/utf8.git/objects/28/d579e4920fbf4f66e71dab3e779d9fbf41422a
new file mode 100644
index 0000000000..413ef4cea0
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/28/d579e4920fbf4f66e71dab3e779d9fbf41422a
@@ -0,0 +1,3 @@
+x•ŽAjÄ0 E»ö)´œR(²ci<PJ{Å–˜)ILo_ÓM×]}xü/×u½uDO½©‚/¤ˆ6!É9 K’KT¦Hœ
+—À(EæäviºuÐÙ|dK8YÎsöñìÕЗd‚¦> ¢ì…œÜûµ6¸Ú¼ý·>dÝ}Íu}OñÄ)xÁ€èi]ÿ%¹ÏRàKvÙôP°Û¢Ð똛lù
+u[¾ád£§ëÑÇ£>»²´QÑ \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/810dbf6b96afaa8c5f69a8b6ec1dabfca7368b b/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/810dbf6b96afaa8c5f69a8b6ec1dabfca7368b
new file mode 100644
index 0000000000..4f6634b5f7
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/810dbf6b96afaa8c5f69a8b6ec1dabfca7368b
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/a73c3499bff049a352b4e265575373e964b89a b/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/a73c3499bff049a352b4e265575373e964b89a
new file mode 100644
index 0000000000..0fcdfdfe72
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/a73c3499bff049a352b4e265575373e964b89a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/c6084110205f98174c4f1ec7e78cb21a15dfc2 b/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/c6084110205f98174c4f1ec7e78cb21a15dfc2
new file mode 100644
index 0000000000..6d9e6b3758
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/3a/c6084110205f98174c4f1ec7e78cb21a15dfc2
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/4c/61dd0a799e0830e77edfe6c74f7c349bc8e62a b/tests/gitea-repositories-meta/user2/utf8.git/objects/4c/61dd0a799e0830e77edfe6c74f7c349bc8e62a
new file mode 100644
index 0000000000..17b3104773
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/4c/61dd0a799e0830e77edfe6c74f7c349bc8e62a
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/50/4d9fe743979d4e9785a25a363c7007293f0838 b/tests/gitea-repositories-meta/user2/utf8.git/objects/50/4d9fe743979d4e9785a25a363c7007293f0838
new file mode 100644
index 0000000000..25794ae805
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/50/4d9fe743979d4e9785a25a363c7007293f0838
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/56/92bcf9f7c9eacb1ad68442161f2573877f96f4 b/tests/gitea-repositories-meta/user2/utf8.git/objects/56/92bcf9f7c9eacb1ad68442161f2573877f96f4
new file mode 100644
index 0000000000..36c0db1ece
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/56/92bcf9f7c9eacb1ad68442161f2573877f96f4
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/59/e2c41e8f5140bb0182acebec17c8ad9831cc62 b/tests/gitea-repositories-meta/user2/utf8.git/objects/59/e2c41e8f5140bb0182acebec17c8ad9831cc62
new file mode 100644
index 0000000000..736a24227c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/59/e2c41e8f5140bb0182acebec17c8ad9831cc62
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/64/89894ad11093fdc49c0ed857d80682344a7264 b/tests/gitea-repositories-meta/user2/utf8.git/objects/64/89894ad11093fdc49c0ed857d80682344a7264
new file mode 100644
index 0000000000..87e198aa9c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/64/89894ad11093fdc49c0ed857d80682344a7264
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/6d/0c79ce3401c67d1ad522e61c47083a9fdee16c b/tests/gitea-repositories-meta/user2/utf8.git/objects/6d/0c79ce3401c67d1ad522e61c47083a9fdee16c
new file mode 100644
index 0000000000..dab81f8d11
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/6d/0c79ce3401c67d1ad522e61c47083a9fdee16c
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/84/7c6d93c6860dd377651245711b7fbcd34a18d4 b/tests/gitea-repositories-meta/user2/utf8.git/objects/84/7c6d93c6860dd377651245711b7fbcd34a18d4
new file mode 100644
index 0000000000..ffea321c19
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/84/7c6d93c6860dd377651245711b7fbcd34a18d4
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/9b/9cc8f558d1c4f815592496fa24308ba2a9c824 b/tests/gitea-repositories-meta/user2/utf8.git/objects/9b/9cc8f558d1c4f815592496fa24308ba2a9c824
new file mode 100644
index 0000000000..8f033d5ae7
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/9b/9cc8f558d1c4f815592496fa24308ba2a9c824
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/a4/f1bb3f2f8c6a0e840e935812ef4903ce515dad b/tests/gitea-repositories-meta/user2/utf8.git/objects/a4/f1bb3f2f8c6a0e840e935812ef4903ce515dad
new file mode 100644
index 0000000000..9655a74c83
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/a4/f1bb3f2f8c6a0e840e935812ef4903ce515dad
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/a9/a61830fbf4e84999d3b20cf178954366701fe5 b/tests/gitea-repositories-meta/user2/utf8.git/objects/a9/a61830fbf4e84999d3b20cf178954366701fe5
new file mode 100644
index 0000000000..a2ceb0044a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/a9/a61830fbf4e84999d3b20cf178954366701fe5
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/c7/85b65bf16928b58567cb23669125c0ccd25a4f b/tests/gitea-repositories-meta/user2/utf8.git/objects/c7/85b65bf16928b58567cb23669125c0ccd25a4f
new file mode 100644
index 0000000000..2cc606b7f2
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/c7/85b65bf16928b58567cb23669125c0ccd25a4f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/e9/63733b8a355cf860c465b4af7b236a6ef08783 b/tests/gitea-repositories-meta/user2/utf8.git/objects/e9/63733b8a355cf860c465b4af7b236a6ef08783
new file mode 100644
index 0000000000..8d16f34e59
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/e9/63733b8a355cf860c465b4af7b236a6ef08783
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/eb/f146f803fccbc1471ef01d8fa0fe12c14e61a5 b/tests/gitea-repositories-meta/user2/utf8.git/objects/eb/f146f803fccbc1471ef01d8fa0fe12c14e61a5
new file mode 100644
index 0000000000..eec8265be4
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/eb/f146f803fccbc1471ef01d8fa0fe12c14e61a5
@@ -0,0 +1 @@
+x•Ž]jC!…ûì*æ½PtFÇ+”Ò.e4#®¹Ášì¾¶;èÓÃùùÊÑ{›€ž_æPÕÄ—«OXÙÓær­±bÚ2Š+Åyg®2ô2D"ò)­õI(`öŠB iÍù¼%1rŸçcÀý¦áýO>õ!ýºë[9ú¸@‰-!¼Z´Ö,w¡MýWÉ|NPÛ®ðÝæÊs´}o.Òæñûrƒ¡UQóˆM‡ \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/ee/9686cb562f492f64381bff7f298b2a1c67a141 b/tests/gitea-repositories-meta/user2/utf8.git/objects/ee/9686cb562f492f64381bff7f298b2a1c67a141
new file mode 100644
index 0000000000..013c499793
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/ee/9686cb562f492f64381bff7f298b2a1c67a141
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/f4/02ff67c0b3161c3988dbf6188e6e0df257fd75 b/tests/gitea-repositories-meta/user2/utf8.git/objects/f4/02ff67c0b3161c3988dbf6188e6e0df257fd75
new file mode 100644
index 0000000000..4ce3cc4136
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/f4/02ff67c0b3161c3988dbf6188e6e0df257fd75
Binary files differ
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/objects/info/packs b/tests/gitea-repositories-meta/user2/utf8.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Grüßen b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Grüßen
new file mode 100644
index 0000000000..abd33644f8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Grüßen
@@ -0,0 +1 @@
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Plus+Is+Not+Space b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Plus+Is+Not+Space
new file mode 100644
index 0000000000..c2850d4c4d
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/Plus+Is+Not+Space
@@ -0,0 +1 @@
+59e2c41e8f5140bb0182acebec17c8ad9831cc62
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/master b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/master
new file mode 100644
index 0000000000..560458b5aa
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/master
@@ -0,0 +1 @@
+3aa73c3499bff049a352b4e265575373e964b89a
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ГлавнаÑВетка b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ГлавнаÑВетка
new file mode 100644
index 0000000000..abd33644f8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ГлавнаÑВетка
@@ -0,0 +1 @@
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/а/б/в b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/а/б/в
new file mode 100644
index 0000000000..abd33644f8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/а/б/в
@@ -0,0 +1 @@
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ブランムb/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ブランãƒ
new file mode 100644
index 0000000000..b0935a9121
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/heads/ブランãƒ
@@ -0,0 +1 @@
+28d579e4920fbf4f66e71dab3e779d9fbf41422a
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/Ð/人 b/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/Ð/人
new file mode 100644
index 0000000000..abd33644f8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/Ð/人
@@ -0,0 +1 @@
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ТÑг b/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ТÑг
new file mode 100644
index 0000000000..abd33644f8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ТÑг
@@ -0,0 +1 @@
+ebf146f803fccbc1471ef01d8fa0fe12c14e61a5
diff --git a/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ã‚¿ã‚° b/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ã‚¿ã‚°
new file mode 100644
index 0000000000..b0935a9121
--- /dev/null
+++ b/tests/gitea-repositories-meta/user2/utf8.git/refs/tags/ã‚¿ã‚°
@@ -0,0 +1 @@
+28d579e4920fbf4f66e71dab3e779d9fbf41422a
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/HEAD b/tests/gitea-repositories-meta/user27/repo49.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/config b/tests/gitea-repositories-meta/user27/repo49.git/config
new file mode 100644
index 0000000000..64280b806c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = true
+ symlinks = false
+ ignorecase = true
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/description b/tests/gitea-repositories-meta/user27/repo49.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive b/tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive
new file mode 100644
index 0000000000..f1f2709ddd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+data=$(cat)
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+echo "${data}" | "${hook}"
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive.d/gitea
new file mode 100644
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive b/tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive
new file mode 100644
index 0000000000..f1f2709ddd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+data=$(cat)
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+echo "${data}" | "${hook}"
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive.d/gitea
new file mode 100644
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/hooks/update b/tests/gitea-repositories-meta/user27/repo49.git/hooks/update
new file mode 100644
index 0000000000..df5bd27f10
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/hooks/update
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+"${hook}" $1 $2 $3
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user27/repo49.git/hooks/update.d/gitea
new file mode 100644
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/info/exclude b/tests/gitea-repositories-meta/user27/repo49.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/info/refs b/tests/gitea-repositories-meta/user27/repo49.git/info/refs
new file mode 100644
index 0000000000..22f08279c0
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/info/refs
@@ -0,0 +1 @@
+aacbdfe9e1c4b47f60abe81849045fa4e96f1d75 refs/heads/master
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3 b/tests/gitea-repositories-meta/user27/repo49.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3
new file mode 100644
index 0000000000..b6f121a4bb
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d b/tests/gitea-repositories-meta/user27/repo49.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d
new file mode 100644
index 0000000000..d2f4c1d04e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061 b/tests/gitea-repositories-meta/user27/repo49.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061
new file mode 100644
index 0000000000..aa34a8a767
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52 b/tests/gitea-repositories-meta/user27/repo49.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52
new file mode 100644
index 0000000000..3f9705fef4
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75 b/tests/gitea-repositories-meta/user27/repo49.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75
new file mode 100644
index 0000000000..74419f4b47
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb b/tests/gitea-repositories-meta/user27/repo49.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb
new file mode 100644
index 0000000000..844eb1c2a6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f b/tests/gitea-repositories-meta/user27/repo49.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f
new file mode 100644
index 0000000000..0699bff833
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/objects/info/packs b/tests/gitea-repositories-meta/user27/repo49.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/refs/heads/master b/tests/gitea-repositories-meta/user27/repo49.git/refs/heads/master
new file mode 100644
index 0000000000..0f13243bfd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/refs/heads/master
@@ -0,0 +1 @@
+aacbdfe9e1c4b47f60abe81849045fa4e96f1d75
diff --git a/tests/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive b/tests/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive
new file mode 100644
index 0000000000..0f13243bfd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive
@@ -0,0 +1 @@
+aacbdfe9e1c4b47f60abe81849045fa4e96f1d75
diff --git a/tests/gitea-repositories-meta/user27/template1.git/HEAD b/tests/gitea-repositories-meta/user27/template1.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user27/template1.git/config b/tests/gitea-repositories-meta/user27/template1.git/config
new file mode 100644
index 0000000000..64280b806c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = true
+ symlinks = false
+ ignorecase = true
diff --git a/tests/gitea-repositories-meta/user27/template1.git/description b/tests/gitea-repositories-meta/user27/template1.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive b/tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive
new file mode 100644
index 0000000000..f1f2709ddd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+data=$(cat)
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+echo "${data}" | "${hook}"
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea
new file mode 100644
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive b/tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive
new file mode 100644
index 0000000000..f1f2709ddd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+data=$(cat)
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+echo "${data}" | "${hook}"
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea
new file mode 100644
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user27/template1.git/hooks/update b/tests/gitea-repositories-meta/user27/template1.git/hooks/update
new file mode 100644
index 0000000000..df5bd27f10
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/hooks/update
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)}
+
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+test -x "${hook}" && test -f "${hook}" || continue
+"${hook}" $1 $2 $3
+exitcodes="${exitcodes} $?"
+done
+
+for i in ${exitcodes}; do
+[ ${i} -eq 0 ] || exit ${i}
+done
diff --git a/tests/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea
new file mode 100644
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user27/template1.git/info/exclude b/tests/gitea-repositories-meta/user27/template1.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user27/template1.git/info/refs b/tests/gitea-repositories-meta/user27/template1.git/info/refs
new file mode 100644
index 0000000000..22f08279c0
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/info/refs
@@ -0,0 +1 @@
+aacbdfe9e1c4b47f60abe81849045fa4e96f1d75 refs/heads/master
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3 b/tests/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3
new file mode 100644
index 0000000000..b6f121a4bb
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d b/tests/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d
new file mode 100644
index 0000000000..d2f4c1d04e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061 b/tests/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061
new file mode 100644
index 0000000000..aa34a8a767
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52 b/tests/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52
new file mode 100644
index 0000000000..3f9705fef4
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75 b/tests/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75
new file mode 100644
index 0000000000..74419f4b47
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb b/tests/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb
new file mode 100644
index 0000000000..844eb1c2a6
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f b/tests/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f
new file mode 100644
index 0000000000..0699bff833
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user27/template1.git/objects/info/packs b/tests/gitea-repositories-meta/user27/template1.git/objects/info/packs
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/objects/info/packs
@@ -0,0 +1 @@
+
diff --git a/tests/gitea-repositories-meta/user27/template1.git/refs/heads/master b/tests/gitea-repositories-meta/user27/template1.git/refs/heads/master
new file mode 100644
index 0000000000..0f13243bfd
--- /dev/null
+++ b/tests/gitea-repositories-meta/user27/template1.git/refs/heads/master
@@ -0,0 +1 @@
+aacbdfe9e1c4b47f60abe81849045fa4e96f1d75
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/HEAD b/tests/gitea-repositories-meta/user3/repo3.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/config b/tests/gitea-repositories-meta/user3/repo3.git/config
new file mode 100644
index 0000000000..e6da231579
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
+ ignorecase = true
+ precomposeunicode = true
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/description b/tests/gitea-repositories-meta/user3/repo3.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive b/tests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive
new file mode 100755
index 0000000000..4b3d452abc
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/post-receive.d"`; do
+ sh "$SHELL_FOLDER/post-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive.d/gitea
new file mode 100755
index 0000000000..43a948da3a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/hooks/post-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive b/tests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive
new file mode 100755
index 0000000000..4127013053
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do
+ sh "$SHELL_FOLDER/pre-receive.d/$i"
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive.d/gitea
new file mode 100755
index 0000000000..49d0940636
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/hooks/pre-receive.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/hooks/update b/tests/gitea-repositories-meta/user3/repo3.git/hooks/update
new file mode 100755
index 0000000000..c186fe4a18
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/hooks/update
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+ORI_DIR=`pwd`
+SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)
+cd "$ORI_DIR"
+for i in `ls "$SHELL_FOLDER/update.d"`; do
+ sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3
+done \ No newline at end of file
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/user3/repo3.git/hooks/update.d/gitea
new file mode 100755
index 0000000000..38101c2426
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/hooks/update.d/gitea
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/info/exclude b/tests/gitea-repositories-meta/user3/repo3.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/20/ade30d25e0ecaeec84e7f542a8456900858240 b/tests/gitea-repositories-meta/user3/repo3.git/objects/20/ade30d25e0ecaeec84e7f542a8456900858240
new file mode 100644
index 0000000000..9f3ffe5f27
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/20/ade30d25e0ecaeec84e7f542a8456900858240
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/27/74debeea6dc742cc4971a92db0e08b95b60588 b/tests/gitea-repositories-meta/user3/repo3.git/objects/27/74debeea6dc742cc4971a92db0e08b95b60588
new file mode 100644
index 0000000000..5d9226f7a1
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/27/74debeea6dc742cc4971a92db0e08b95b60588
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/2a/47ca4b614a9f5a43abbd5ad851a54a616ffee6 b/tests/gitea-repositories-meta/user3/repo3.git/objects/2a/47ca4b614a9f5a43abbd5ad851a54a616ffee6
new file mode 100644
index 0000000000..ca60d2314f
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/2a/47ca4b614a9f5a43abbd5ad851a54a616ffee6
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/2f/9b22fd3159a43b7b4e5dd806fcd544edf8716f b/tests/gitea-repositories-meta/user3/repo3.git/objects/2f/9b22fd3159a43b7b4e5dd806fcd544edf8716f
new file mode 100644
index 0000000000..e98d752dad
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/2f/9b22fd3159a43b7b4e5dd806fcd544edf8716f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/d2/2b4d4daa5be07329fcef6ed458f00cf3392da0 b/tests/gitea-repositories-meta/user3/repo3.git/objects/d2/2b4d4daa5be07329fcef6ed458f00cf3392da0
new file mode 100644
index 0000000000..e319f8ce34
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/d2/2b4d4daa5be07329fcef6ed458f00cf3392da0
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/d5/6a3073c1dbb7b15963110a049d50cdb5db99fc b/tests/gitea-repositories-meta/user3/repo3.git/objects/d5/6a3073c1dbb7b15963110a049d50cdb5db99fc
new file mode 100644
index 0000000000..eff3c9833e
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/d5/6a3073c1dbb7b15963110a049d50cdb5db99fc
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/ec/f0db3c1ec806522de4b491fb9a3c7457398c61 b/tests/gitea-repositories-meta/user3/repo3.git/objects/ec/f0db3c1ec806522de4b491fb9a3c7457398c61
new file mode 100644
index 0000000000..ed431f70d3
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/ec/f0db3c1ec806522de4b491fb9a3c7457398c61
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/objects/ee/16d127df463aa491e08958120f2108b02468df b/tests/gitea-repositories-meta/user3/repo3.git/objects/ee/16d127df463aa491e08958120f2108b02468df
new file mode 100644
index 0000000000..e177f69e37
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/objects/ee/16d127df463aa491e08958120f2108b02468df
Binary files differ
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/refs/heads/master b/tests/gitea-repositories-meta/user3/repo3.git/refs/heads/master
new file mode 100644
index 0000000000..ccee722d52
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/refs/heads/master
@@ -0,0 +1 @@
+2a47ca4b614a9f5a43abbd5ad851a54a616ffee6
diff --git a/tests/gitea-repositories-meta/user3/repo3.git/refs/heads/test_branch b/tests/gitea-repositories-meta/user3/repo3.git/refs/heads/test_branch
new file mode 100644
index 0000000000..dfe0c6a128
--- /dev/null
+++ b/tests/gitea-repositories-meta/user3/repo3.git/refs/heads/test_branch
@@ -0,0 +1 @@
+d22b4d4daa5be07329fcef6ed458f00cf3392da0
diff --git a/tests/gitea-repositories-meta/user30/empty.git/HEAD b/tests/gitea-repositories-meta/user30/empty.git/HEAD
new file mode 100644
index 0000000000..b870d82622
--- /dev/null
+++ b/tests/gitea-repositories-meta/user30/empty.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/main
diff --git a/tests/gitea-repositories-meta/user30/empty.git/config b/tests/gitea-repositories-meta/user30/empty.git/config
new file mode 100644
index 0000000000..7c968c3b5a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user30/empty.git/config
@@ -0,0 +1,6 @@
+[core]
+ bare = true
+ repositoryformatversion = 0
+ filemode = false
+ symlinks = false
+ ignorecase = true
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/HEAD b/tests/gitea-repositories-meta/user5/repo4.git/HEAD
new file mode 100644
index 0000000000..cb089cd89a
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/config b/tests/gitea-repositories-meta/user5/repo4.git/config
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/description b/tests/gitea-repositories-meta/user5/repo4.git/description
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/info/exclude b/tests/gitea-repositories-meta/user5/repo4.git/info/exclude
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/objects/16/dfebd1ed3905d78d7e061e945fc9c34afe4e81 b/tests/gitea-repositories-meta/user5/repo4.git/objects/16/dfebd1ed3905d78d7e061e945fc9c34afe4e81
new file mode 100644
index 0000000000..76d765ea90
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/objects/16/dfebd1ed3905d78d7e061e945fc9c34afe4e81
Binary files differ
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/objects/c1/202ad022ae7d3a6d2474dc76d5a0c8e87cdc0f b/tests/gitea-repositories-meta/user5/repo4.git/objects/c1/202ad022ae7d3a6d2474dc76d5a0c8e87cdc0f
new file mode 100644
index 0000000000..f63d6019b8
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/objects/c1/202ad022ae7d3a6d2474dc76d5a0c8e87cdc0f
Binary files differ
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/objects/c7/cd3cd144e6d23c9d6f3d07e52b2c1a956e0338 b/tests/gitea-repositories-meta/user5/repo4.git/objects/c7/cd3cd144e6d23c9d6f3d07e52b2c1a956e0338
new file mode 100644
index 0000000000..c8d7c54d58
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/objects/c7/cd3cd144e6d23c9d6f3d07e52b2c1a956e0338
Binary files differ
diff --git a/tests/gitea-repositories-meta/user5/repo4.git/refs/heads/master b/tests/gitea-repositories-meta/user5/repo4.git/refs/heads/master
new file mode 100644
index 0000000000..5fd26e37da
--- /dev/null
+++ b/tests/gitea-repositories-meta/user5/repo4.git/refs/heads/master
@@ -0,0 +1 @@
+c7cd3cd144e6d23c9d6f3d07e52b2c1a956e0338
diff --git a/tests/integration/README.md b/tests/integration/README.md
new file mode 100644
index 0000000000..636949df38
--- /dev/null
+++ b/tests/integration/README.md
@@ -0,0 +1,96 @@
+# Integration tests
+
+Integration tests can be run with make commands for the
+appropriate backends, namely:
+```shell
+make test-sqlite
+make test-pgsql
+make test-mysql
+make test-mysql8
+make test-mssql
+```
+
+Make sure to perform a clean build before running tests:
+```
+make clean build
+```
+
+## Run all tests via local drone
+```
+drone exec --local --build-event "pull_request"
+```
+
+## Run sqlite integration tests
+Start tests
+```
+make test-sqlite
+```
+
+## Run MySQL integration tests
+Setup a MySQL database inside docker
+```
+docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest #(just ctrl-c to stop db and clean the container)
+docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --rm --name elasticsearch elasticsearch:7.6.0 #(in a second terminal, just ctrl-c to stop db and clean the container)
+```
+Start tests based on the database container
+```
+TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-mysql
+```
+
+## Run pgsql integration tests
+Setup a pgsql database inside docker
+```
+docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:latest #(just ctrl-c to stop db and clean the container)
+```
+Start tests based on the database container
+```
+TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-pgsql
+```
+
+## Run mssql integration tests
+Setup a mssql database inside docker
+```
+docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
+```
+Start tests based on the database container
+```
+TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql
+```
+
+## Running individual tests
+
+Example command to run GPG test:
+
+For SQLite:
+
+```
+make test-sqlite#GPG
+```
+
+For other databases(replace `mssql` to `mysql`, `mysql8` or `pgsql`):
+
+```
+TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql#GPG
+```
+
+## Setting timeouts for declaring long-tests and long-flushes
+
+We appreciate that some testing machines may not be very powerful and
+the default timeouts for declaring a slow test or a slow clean-up flush
+may not be appropriate.
+
+You can either:
+
+* Within the test ini file set the following section:
+
+```ini
+[integration-tests]
+SLOW_TEST = 10s ; 10s is the default value
+SLOW_FLUSH = 5S ; 5s is the default value
+```
+
+* Set the following environment variables:
+
+```bash
+GITEA_SLOW_TEST_TIME="10s" GITEA_SLOW_FLUSH_TIME="5s" make test-sqlite
+```
diff --git a/tests/integration/README_ZH.md b/tests/integration/README_ZH.md
new file mode 100644
index 0000000000..3840232472
--- /dev/null
+++ b/tests/integration/README_ZH.md
@@ -0,0 +1,71 @@
+# 关于集æˆæµ‹è¯•
+
+使用如下 make 命令å¯ä»¥è¿è¡ŒæŒ‡å®šçš„集æˆæµ‹è¯•ï¼š
+```shell
+make test-mysql
+make test-pgsql
+make test-sqlite
+```
+
+在执行集æˆæµ‹è¯•å‘½ä»¤å‰è¯·ç¡®ä¿æ¸…ç†äº†ä¹‹å‰çš„构建环境,清ç†å‘½ä»¤å¦‚下:
+```
+make clean build
+```
+
+## 如何在本地 drone æœåŠ¡å™¨ä¸Šè¿è¡Œæ‰€æœ‰æµ‹è¯•
+```
+drone exec --local --build-event "pull_request"
+```
+
+## 如何使用 sqlite æ•°æ®åº“进行集æˆæµ‹è¯•
+使用该命令执行集æˆæµ‹è¯•
+```
+make test-sqlite
+```
+
+## 如何使用 mysql æ•°æ®åº“进行集æˆæµ‹è¯•
+首先在docker容器里部署一个 mysql æ•°æ®åº“
+```
+docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:8 #(just ctrl-c to stop db and clean the container)
+```
+之åŽä¾¿å¯ä»¥åŸºäºŽè¿™ä¸ªæ•°æ®åº“进行集æˆæµ‹è¯•
+```
+TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-mysql
+```
+
+## 如何使用 pgsql æ•°æ®åº“进行集æˆæµ‹è¯•
+åŒä¸Šï¼Œé¦–先在 docker 容器里部署一个 pgsql æ•°æ®åº“
+```
+docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:14 #(just ctrl-c to stop db and clean the container)
+```
+之åŽä¾¿å¯ä»¥åŸºäºŽè¿™ä¸ªæ•°æ®åº“进行集æˆæµ‹è¯•
+```
+TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-pgsql
+```
+
+## Run mssql integration tests
+åŒä¸Šï¼Œé¦–先在 docker 容器里部署一个 mssql æ•°æ®åº“
+```
+docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
+```
+之åŽä¾¿å¯ä»¥åŸºäºŽè¿™ä¸ªæ•°æ®åº“进行集æˆæµ‹è¯•
+```
+TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql
+```
+
+## 如何进行自定义的集æˆæµ‹è¯•
+
+下é¢çš„示例展示了怎样在集æˆæµ‹è¯•ä¸­åªè¿›è¡Œ GPG 测试:
+
+sqlite æ•°æ®åº“:
+
+```
+make test-sqlite#GPG
+```
+
+其它数æ®åº“(把 MSSQL 替æ¢ä¸º MYSQL, MYSQL8, PGSQL):
+
+```
+TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql#GPG
+```
+
diff --git a/tests/integration/admin_user_test.go b/tests/integration/admin_user_test.go
new file mode 100644
index 0000000000..ffe3f670fe
--- /dev/null
+++ b/tests/integration/admin_user_test.go
@@ -0,0 +1,84 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "strconv"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAdminViewUsers(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+ req := NewRequest(t, "GET", "/admin/users")
+ session.MakeRequest(t, req, http.StatusOK)
+
+ session = loginUser(t, "user2")
+ req = NewRequest(t, "GET", "/admin/users")
+ session.MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAdminViewUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+ req := NewRequest(t, "GET", "/admin/users/1")
+ session.MakeRequest(t, req, http.StatusOK)
+
+ session = loginUser(t, "user2")
+ req = NewRequest(t, "GET", "/admin/users/1")
+ session.MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAdminEditUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ testSuccessfullEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
+}
+
+func testSuccessfullEdit(t *testing.T, formData user_model.User) {
+ makeRequest(t, formData, http.StatusSeeOther)
+}
+
+func makeRequest(t *testing.T, formData user_model.User, headerCode int) {
+ session := loginUser(t, "user1")
+ csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID)))
+ req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID)), map[string]string{
+ "_csrf": csrf,
+ "user_name": formData.Name,
+ "login_name": formData.LoginName,
+ "login_type": "0-0",
+ "email": formData.Email,
+ })
+
+ session.MakeRequest(t, req, headerCode)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: formData.ID})
+ assert.Equal(t, formData.Name, user.Name)
+ assert.Equal(t, formData.LoginName, user.LoginName)
+ assert.Equal(t, formData.Email, user.Email)
+}
+
+func TestAdminDeleteUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+
+ csrf := GetCSRF(t, session, "/admin/users/8")
+ req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{
+ "_csrf": csrf,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ assertUserDeleted(t, 8)
+ unittest.CheckConsistencyFor(t, &user_model.User{})
+}
diff --git a/tests/integration/api_activitypub_person_test.go b/tests/integration/api_activitypub_person_test.go
new file mode 100644
index 0000000000..e7ef79d156
--- /dev/null
+++ b/tests/integration/api_activitypub_person_test.go
@@ -0,0 +1,113 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/activitypub"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers"
+
+ ap "github.com/go-ap/activitypub"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestActivityPubPerson(t *testing.T) {
+ setting.Federation.Enabled = true
+ c = routers.NormalRoutes(context.TODO())
+ defer func() {
+ setting.Federation.Enabled = false
+ c = routers.NormalRoutes(context.TODO())
+ }()
+
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ username := "user2"
+ req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username))
+ resp := MakeRequest(t, req, http.StatusOK)
+ body := resp.Body.Bytes()
+ assert.Contains(t, string(body), "@context")
+
+ var person ap.Person
+ err := person.UnmarshalJSON(body)
+ assert.NoError(t, err)
+
+ assert.Equal(t, ap.PersonType, person.Type)
+ assert.Equal(t, username, person.PreferredUsername.String())
+ keyID := person.GetID().String()
+ assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyID)
+ assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.Outbox.GetID().String())
+ assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.Inbox.GetID().String())
+
+ pubKey := person.PublicKey
+ assert.NotNil(t, pubKey)
+ publicKeyID := keyID + "#main-key"
+ assert.Equal(t, pubKey.ID.String(), publicKeyID)
+
+ pubKeyPem := pubKey.PublicKeyPem
+ assert.NotNil(t, pubKeyPem)
+ assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
+ })
+}
+
+func TestActivityPubMissingPerson(t *testing.T) {
+ setting.Federation.Enabled = true
+ c = routers.NormalRoutes(context.TODO())
+ defer func() {
+ setting.Federation.Enabled = false
+ c = routers.NormalRoutes(context.TODO())
+ }()
+
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser")
+ resp := MakeRequest(t, req, http.StatusNotFound)
+ assert.Contains(t, resp.Body.String(), "user redirect does not exist")
+ })
+}
+
+func TestActivityPubPersonInbox(t *testing.T) {
+ setting.Federation.Enabled = true
+ c = routers.NormalRoutes(context.TODO())
+ defer func() {
+ setting.Federation.Enabled = false
+ c = routers.NormalRoutes(context.TODO())
+ }()
+
+ srv := httptest.NewServer(c)
+ defer srv.Close()
+
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ appURL := setting.AppURL
+ setting.AppURL = srv.URL
+ defer func() {
+ setting.Database.LogSQL = false
+ setting.AppURL = appURL
+ }()
+ username1 := "user1"
+ ctx := context.Background()
+ user1, err := user_model.GetUserByName(ctx, username1)
+ assert.NoError(t, err)
+ user1url := fmt.Sprintf("%s/api/v1/activitypub/user/%s#main-key", srv.URL, username1)
+ c, err := activitypub.NewClient(user1, user1url)
+ assert.NoError(t, err)
+ username2 := "user2"
+ user2inboxurl := fmt.Sprintf("%s/api/v1/activitypub/user/%s/inbox", srv.URL, username2)
+
+ // Signed request succeeds
+ resp, err := c.Post([]byte{}, user2inboxurl)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusNoContent, resp.StatusCode)
+
+ // Unsigned request fails
+ req := NewRequest(t, "POST", user2inboxurl)
+ MakeRequest(t, req, http.StatusInternalServerError)
+ })
+}
diff --git a/tests/integration/api_admin_org_test.go b/tests/integration/api_admin_org_test.go
new file mode 100644
index 0000000000..720f6fc6b6
--- /dev/null
+++ b/tests/integration/api_admin_org_test.go
@@ -0,0 +1,88 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIAdminOrgCreate(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+
+ org := api.CreateOrgOption{
+ UserName: "user2_org",
+ FullName: "User2's organization",
+ Description: "This organization created by admin for user2",
+ Website: "https://try.gitea.io",
+ Location: "Shanghai",
+ Visibility: "private",
+ }
+ req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var apiOrg api.Organization
+ DecodeJSON(t, resp, &apiOrg)
+
+ assert.Equal(t, org.UserName, apiOrg.UserName)
+ assert.Equal(t, org.FullName, apiOrg.FullName)
+ assert.Equal(t, org.Description, apiOrg.Description)
+ assert.Equal(t, org.Website, apiOrg.Website)
+ assert.Equal(t, org.Location, apiOrg.Location)
+ assert.Equal(t, org.Visibility, apiOrg.Visibility)
+
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: org.UserName,
+ LowerName: strings.ToLower(org.UserName),
+ FullName: org.FullName,
+ })
+ })
+}
+
+func TestAPIAdminOrgCreateBadVisibility(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+
+ org := api.CreateOrgOption{
+ UserName: "user2_org",
+ FullName: "User2's organization",
+ Description: "This organization created by admin for user2",
+ Website: "https://try.gitea.io",
+ Location: "Shanghai",
+ Visibility: "notvalid",
+ }
+ req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+}
+
+func TestAPIAdminOrgCreateNotAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ nonAdminUsername := "user2"
+ session := loginUser(t, nonAdminUsername)
+ token := getTokenForLoggedInUser(t, session)
+ org := api.CreateOrgOption{
+ UserName: "user2_org",
+ FullName: "User2's organization",
+ Description: "This organization created by admin for user2",
+ Website: "https://try.gitea.io",
+ Location: "Shanghai",
+ Visibility: "public",
+ }
+ req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org)
+ session.MakeRequest(t, req, http.StatusForbidden)
+}
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
new file mode 100644
index 0000000000..dea0bdd063
--- /dev/null
+++ b/tests/integration/api_admin_test.go
@@ -0,0 +1,211 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ // user1 is an admin user
+ session := loginUser(t, "user1")
+ keyOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
+
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys?token=%s", keyOwner.Name, token)
+ req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
+ "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+ "title": "test-key",
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var newPublicKey api.PublicKey
+ DecodeJSON(t, resp, &newPublicKey)
+ unittest.AssertExistsAndLoadBean(t, &asymkey_model.PublicKey{
+ ID: newPublicKey.ID,
+ Name: newPublicKey.Title,
+ Fingerprint: newPublicKey.Fingerprint,
+ OwnerID: keyOwner.ID,
+ })
+
+ req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d?token=%s",
+ keyOwner.Name, newPublicKey.ID, token)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ unittest.AssertNotExistsBean(t, &asymkey_model.PublicKey{ID: newPublicKey.ID})
+}
+
+func TestAPIAdminDeleteMissingSSHKey(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ // user1 is an admin user
+ session := loginUser(t, "user1")
+
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "DELETE", "/api/v1/admin/users/user1/keys/%d?token=%s", unittest.NonexistentID, token)
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ normalUsername := "user2"
+ session := loginUser(t, adminUsername)
+
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys?token=%s", adminUsername, token)
+ req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
+ "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+ "title": "test-key",
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ var newPublicKey api.PublicKey
+ DecodeJSON(t, resp, &newPublicKey)
+
+ session = loginUser(t, normalUsername)
+ token = getTokenForLoggedInUser(t, session)
+ req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d?token=%s",
+ adminUsername, newPublicKey.ID, token)
+ session.MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAPISudoUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ normalUsername := "user2"
+ session := loginUser(t, adminUsername)
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", normalUsername, token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var user api.User
+ DecodeJSON(t, resp, &user)
+
+ assert.Equal(t, normalUsername, user.UserName)
+}
+
+func TestAPISudoUserForbidden(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ normalUsername := "user2"
+
+ session := loginUser(t, normalUsername)
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", adminUsername, token)
+ req := NewRequest(t, "GET", urlStr)
+ session.MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAPIListUsers(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ session := loginUser(t, adminUsername)
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/admin/users?token=%s", token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var users []api.User
+ DecodeJSON(t, resp, &users)
+
+ found := false
+ for _, user := range users {
+ if user.UserName == adminUsername {
+ found = true
+ }
+ }
+ assert.True(t, found)
+ numberOfUsers := unittest.GetCount(t, &user_model.User{}, "type = 0")
+ assert.Equal(t, numberOfUsers, len(users))
+}
+
+func TestAPIListUsersNotLoggedIn(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "GET", "/api/v1/admin/users")
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
+
+func TestAPIListUsersNonAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ nonAdminUsername := "user2"
+ session := loginUser(t, nonAdminUsername)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/admin/users?token=%s", token)
+ session.MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAPICreateUserInvalidEmail(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ session := loginUser(t, adminUsername)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/admin/users?token=%s", token)
+ req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
+ "email": "invalid_email@domain.com\r\n",
+ "full_name": "invalid user",
+ "login_name": "invalidUser",
+ "must_change_password": "true",
+ "password": "password",
+ "send_notify": "true",
+ "source_id": "0",
+ "username": "invalidUser",
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+}
+
+func TestAPIEditUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ session := loginUser(t, adminUsername)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/admin/users/%s?token=%s", "user2", token)
+
+ req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+ // required
+ "login_name": "user2",
+ "source_id": "0",
+ // to change
+ "full_name": "Full Name User 2",
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+
+ empty := ""
+ req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
+ LoginName: "user2",
+ SourceID: 0,
+ Email: &empty,
+ })
+ resp := session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ errMap := make(map[string]interface{})
+ json.Unmarshal(resp.Body.Bytes(), &errMap)
+ assert.EqualValues(t, "email is not allowed to be empty string", errMap["message"].(string))
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
+ assert.False(t, user2.IsRestricted)
+ bTrue := true
+ req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
+ // required
+ LoginName: "user2",
+ SourceID: 0,
+ // to change
+ Restricted: &bTrue,
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+ user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
+ assert.True(t, user2.IsRestricted)
+}
diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go
new file mode 100644
index 0000000000..bdfdd3c752
--- /dev/null
+++ b/tests/integration/api_branch_test.go
@@ -0,0 +1,201 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branches/%s?token=%s", branchName, token)
+ resp := session.MakeRequest(t, req, NoExpectedStatus)
+ if !exists {
+ assert.EqualValues(t, http.StatusNotFound, resp.Code)
+ return
+ }
+ assert.EqualValues(t, http.StatusOK, resp.Code)
+ var branch api.Branch
+ DecodeJSON(t, resp, &branch)
+ assert.EqualValues(t, branchName, branch.Name)
+ assert.True(t, branch.UserCanPush)
+ assert.True(t, branch.UserCanMerge)
+}
+
+func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
+ resp := session.MakeRequest(t, req, expectedHTTPStatus)
+
+ if resp.Code == http.StatusOK {
+ var branchProtection api.BranchProtection
+ DecodeJSON(t, resp, &branchProtection)
+ assert.EqualValues(t, branchName, branchProtection.BranchName)
+ }
+}
+
+func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{
+ BranchName: branchName,
+ })
+ resp := session.MakeRequest(t, req, expectedHTTPStatus)
+
+ if resp.Code == http.StatusCreated {
+ var branchProtection api.BranchProtection
+ DecodeJSON(t, resp, &branchProtection)
+ assert.EqualValues(t, branchName, branchProtection.BranchName)
+ }
+}
+
+func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.BranchProtection, expectedHTTPStatus int) {
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/branch_protections/"+branchName+"?token="+token, body)
+ resp := session.MakeRequest(t, req, expectedHTTPStatus)
+
+ if resp.Code == http.StatusOK {
+ var branchProtection api.BranchProtection
+ DecodeJSON(t, resp, &branchProtection)
+ assert.EqualValues(t, branchName, branchProtection.BranchName)
+ }
+}
+
+func testAPIDeleteBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
+ session.MakeRequest(t, req, expectedHTTPStatus)
+}
+
+func testAPIDeleteBranch(t *testing.T, branchName string, expectedHTTPStatus int) {
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branches/%s?token=%s", branchName, token)
+ session.MakeRequest(t, req, expectedHTTPStatus)
+}
+
+func TestAPIGetBranch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ for _, test := range []struct {
+ BranchName string
+ Exists bool
+ }{
+ {"master", true},
+ {"master/doesnotexist", false},
+ {"feature/1", true},
+ {"feature/1/doesnotexist", false},
+ } {
+ testAPIGetBranch(t, test.BranchName, test.Exists)
+ }
+}
+
+func TestAPICreateBranch(t *testing.T) {
+ onGiteaRun(t, testAPICreateBranches)
+}
+
+func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {
+ username := "user2"
+ ctx := NewAPITestContext(t, username, "my-noo-repo")
+ giteaURL.Path = ctx.GitPath()
+
+ t.Run("CreateRepo", doAPICreateRepository(ctx, false))
+ testCases := []struct {
+ OldBranch string
+ NewBranch string
+ ExpectedHTTPStatus int
+ }{
+ // Creating branch from default branch
+ {
+ OldBranch: "",
+ NewBranch: "new_branch_from_default_branch",
+ ExpectedHTTPStatus: http.StatusCreated,
+ },
+ // Creating branch from master
+ {
+ OldBranch: "master",
+ NewBranch: "new_branch_from_master_1",
+ ExpectedHTTPStatus: http.StatusCreated,
+ },
+ // Trying to create from master but already exists
+ {
+ OldBranch: "master",
+ NewBranch: "new_branch_from_master_1",
+ ExpectedHTTPStatus: http.StatusConflict,
+ },
+ // Trying to create from other branch (not default branch)
+ {
+ OldBranch: "new_branch_from_master_1",
+ NewBranch: "branch_2",
+ ExpectedHTTPStatus: http.StatusCreated,
+ },
+ // Trying to create from a branch which does not exist
+ {
+ OldBranch: "does_not_exist",
+ NewBranch: "new_branch_from_non_existent",
+ ExpectedHTTPStatus: http.StatusNotFound,
+ },
+ }
+ for _, test := range testCases {
+ defer tests.ResetFixtures(t)
+ session := ctx.Session
+ testAPICreateBranch(t, session, "user2", "my-noo-repo", test.OldBranch, test.NewBranch, test.ExpectedHTTPStatus)
+ }
+}
+
+func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBranch, newBranch string, status int) bool {
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+user+"/"+repo+"/branches?token="+token, &api.CreateBranchRepoOption{
+ BranchName: newBranch,
+ OldBranchName: oldBranch,
+ })
+ resp := session.MakeRequest(t, req, status)
+
+ var branch api.Branch
+ DecodeJSON(t, resp, &branch)
+
+ if status == http.StatusCreated {
+ assert.EqualValues(t, newBranch, branch.Name)
+ }
+
+ return resp.Result().StatusCode == status
+}
+
+func TestAPIBranchProtection(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // Branch protection only on branch that exist
+ testAPICreateBranchProtection(t, "master/doesnotexist", http.StatusNotFound)
+ // Get branch protection on branch that exist but not branch protection
+ testAPIGetBranchProtection(t, "master", http.StatusNotFound)
+
+ testAPICreateBranchProtection(t, "master", http.StatusCreated)
+ // Can only create once
+ testAPICreateBranchProtection(t, "master", http.StatusForbidden)
+
+ // Can't delete a protected branch
+ testAPIDeleteBranch(t, "master", http.StatusForbidden)
+
+ testAPIGetBranchProtection(t, "master", http.StatusOK)
+ testAPIEditBranchProtection(t, "master", &api.BranchProtection{
+ EnablePush: true,
+ }, http.StatusOK)
+
+ testAPIDeleteBranchProtection(t, "master", http.StatusNoContent)
+
+ // Test branch deletion
+ testAPIDeleteBranch(t, "master", http.StatusForbidden)
+ testAPIDeleteBranch(t, "branch2", http.StatusNoContent)
+}
diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go
new file mode 100644
index 0000000000..126d886842
--- /dev/null
+++ b/tests/integration/api_comment_test.go
@@ -0,0 +1,205 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/convert"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIListRepoComments(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+ unittest.Cond("type = ?", issues_model.CommentTypeComment))
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments", repoOwner.Name, repo.Name))
+ req := NewRequest(t, "GET", link.String())
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiComments []*api.Comment
+ DecodeJSON(t, resp, &apiComments)
+ assert.Len(t, apiComments, 2)
+ for _, apiComment := range apiComments {
+ c := &issues_model.Comment{ID: apiComment.ID}
+ unittest.AssertExistsAndLoadBean(t, c,
+ unittest.Cond("type = ?", issues_model.CommentTypeComment))
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: c.IssueID, RepoID: repo.ID})
+ }
+
+ // test before and since filters
+ query := url.Values{}
+ before := "2000-01-01T00:00:11+00:00" // unix: 946684811
+ since := "2000-01-01T00:00:12+00:00" // unix: 946684812
+ query.Add("before", before)
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiComments)
+ assert.Len(t, apiComments, 1)
+ assert.EqualValues(t, 2, apiComments[0].ID)
+
+ query.Del("before")
+ query.Add("since", since)
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiComments)
+ assert.Len(t, apiComments, 1)
+ assert.EqualValues(t, 3, apiComments[0].ID)
+}
+
+func TestAPIListIssueComments(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+ unittest.Cond("type = ?", issues_model.CommentTypeComment))
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/comments",
+ repoOwner.Name, repo.Name, issue.Index)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var comments []*api.Comment
+ DecodeJSON(t, resp, &comments)
+ expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID},
+ unittest.Cond("type = ?", issues_model.CommentTypeComment))
+ assert.EqualValues(t, expectedCount, len(comments))
+}
+
+func TestAPICreateComment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ const commentBody = "Comment body"
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
+ repoOwner.Name, repo.Name, issue.Index, token)
+ req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
+ "body": commentBody,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var updatedComment api.Comment
+ DecodeJSON(t, resp, &updatedComment)
+ assert.EqualValues(t, commentBody, updatedComment.Body)
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
+}
+
+func TestAPIGetComment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
+ assert.NoError(t, comment.LoadIssue())
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID)
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiComment api.Comment
+ DecodeJSON(t, resp, &apiComment)
+
+ assert.NoError(t, comment.LoadPoster())
+ expect := convert.ToComment(comment)
+
+ assert.Equal(t, expect.ID, apiComment.ID)
+ assert.Equal(t, expect.Poster.FullName, apiComment.Poster.FullName)
+ assert.Equal(t, expect.Body, apiComment.Body)
+ assert.Equal(t, expect.Created.Unix(), apiComment.Created.Unix())
+}
+
+func TestAPIEditComment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ const newCommentBody = "This is the new comment body"
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+ unittest.Cond("type = ?", issues_model.CommentTypeComment))
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
+ repoOwner.Name, repo.Name, comment.ID, token)
+ req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+ "body": newCommentBody,
+ })
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var updatedComment api.Comment
+ DecodeJSON(t, resp, &updatedComment)
+ assert.EqualValues(t, comment.ID, updatedComment.ID)
+ assert.EqualValues(t, newCommentBody, updatedComment.Body)
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
+}
+
+func TestAPIDeleteComment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
+ unittest.Cond("type = ?", issues_model.CommentTypeComment))
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
+ repoOwner.Name, repo.Name, comment.ID, token)
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
+}
+
+func TestAPIListIssueTimeline(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // load comment
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ // make request
+ session := loginUser(t, repoOwner.Name)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/timeline",
+ repoOwner.Name, repo.Name, issue.Index)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ // check if lens of list returned by API and
+ // lists extracted directly from DB are the same
+ var comments []*api.TimelineComment
+ DecodeJSON(t, resp, &comments)
+ expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID})
+ assert.EqualValues(t, expectedCount, len(comments))
+}
diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go
new file mode 100644
index 0000000000..131dcf70bb
--- /dev/null
+++ b/tests/integration/api_fork_test.go
@@ -0,0 +1,19 @@
+// Copyright 2017 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestCreateForkNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
diff --git a/tests/integration/api_gpg_keys_test.go b/tests/integration/api_gpg_keys_test.go
new file mode 100644
index 0000000000..0ad876c9b9
--- /dev/null
+++ b/tests/integration/api_gpg_keys_test.go
@@ -0,0 +1,264 @@
+// Copyright 2017 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strconv"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type makeRequestFunc func(testing.TB, *http.Request, int) *httptest.ResponseRecorder
+
+func TestGPGKeys(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+
+ tt := []struct {
+ name string
+ makeRequest makeRequestFunc
+ token string
+ results []int
+ }{
+ {
+ name: "NoLogin", makeRequest: MakeRequest, token: "",
+ results: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
+ },
+ {
+ name: "LoggedAsUser2", makeRequest: session.MakeRequest, token: token,
+ results: []int{http.StatusOK, http.StatusOK, http.StatusNotFound, http.StatusNoContent, http.StatusUnprocessableEntity, http.StatusNotFound, http.StatusCreated, http.StatusNotFound, http.StatusCreated},
+ },
+ }
+
+ for _, tc := range tt {
+ // Basic test on result code
+ t.Run(tc.name, func(t *testing.T) {
+ t.Run("ViewOwnGPGKeys", func(t *testing.T) {
+ testViewOwnGPGKeys(t, tc.makeRequest, tc.token, tc.results[0])
+ })
+ t.Run("ViewGPGKeys", func(t *testing.T) {
+ testViewGPGKeys(t, tc.makeRequest, tc.token, tc.results[1])
+ })
+ t.Run("GetGPGKey", func(t *testing.T) {
+ testGetGPGKey(t, tc.makeRequest, tc.token, tc.results[2])
+ })
+ t.Run("DeleteGPGKey", func(t *testing.T) {
+ testDeleteGPGKey(t, tc.makeRequest, tc.token, tc.results[3])
+ })
+
+ t.Run("CreateInvalidGPGKey", func(t *testing.T) {
+ testCreateInvalidGPGKey(t, tc.makeRequest, tc.token, tc.results[4])
+ })
+ t.Run("CreateNoneRegistredEmailGPGKey", func(t *testing.T) {
+ testCreateNoneRegistredEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[5])
+ })
+ t.Run("CreateValidGPGKey", func(t *testing.T) {
+ testCreateValidGPGKey(t, tc.makeRequest, tc.token, tc.results[6])
+ })
+ t.Run("CreateValidSecondaryEmailGPGKeyNotActivated", func(t *testing.T) {
+ testCreateValidSecondaryEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[7])
+ })
+ })
+ }
+
+ // Check state after basic add
+ t.Run("CheckState", func(t *testing.T) {
+ var keys []*api.GPGKey
+
+ req := NewRequest(t, "GET", "/api/v1/user/gpg_keys?token="+token) // GET all keys
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &keys)
+ assert.Len(t, keys, 1)
+
+ primaryKey1 := keys[0] // Primary key 1
+ assert.EqualValues(t, "38EA3BCED732982C", primaryKey1.KeyID)
+ assert.Len(t, primaryKey1.Emails, 1)
+ assert.EqualValues(t, "user2@example.com", primaryKey1.Emails[0].Email)
+ assert.True(t, primaryKey1.Emails[0].Verified)
+
+ subKey := primaryKey1.SubsKey[0] // Subkey of 38EA3BCED732982C
+ assert.EqualValues(t, "70D7C694D17D03AD", subKey.KeyID)
+ assert.Empty(t, subKey.Emails)
+
+ var key api.GPGKey
+ req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(primaryKey1.ID, 10)+"?token="+token) // Primary key 1
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &key)
+ assert.EqualValues(t, "38EA3BCED732982C", key.KeyID)
+ assert.Len(t, key.Emails, 1)
+ assert.EqualValues(t, "user2@example.com", key.Emails[0].Email)
+ assert.True(t, key.Emails[0].Verified)
+
+ req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(subKey.ID, 10)+"?token="+token) // Subkey of 38EA3BCED732982C
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &key)
+ assert.EqualValues(t, "70D7C694D17D03AD", key.KeyID)
+ assert.Empty(t, key.Emails)
+ })
+
+ // Check state after basic add
+ t.Run("CheckCommits", func(t *testing.T) {
+ t.Run("NotSigned", func(t *testing.T) {
+ var branch api.Branch
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/branches/not-signed?token="+token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &branch)
+ assert.False(t, branch.Commit.Verification.Verified)
+ })
+
+ t.Run("SignedWithNotValidatedEmail", func(t *testing.T) {
+ var branch api.Branch
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/branches/good-sign-not-yet-validated?token="+token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &branch)
+ assert.False(t, branch.Commit.Verification.Verified)
+ })
+
+ t.Run("SignedWithValidEmail", func(t *testing.T) {
+ var branch api.Branch
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/branches/good-sign?token="+token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &branch)
+ assert.True(t, branch.Commit.Verification.Verified)
+ })
+ })
+}
+
+func testViewOwnGPGKeys(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ req := NewRequest(t, "GET", "/api/v1/user/gpg_keys?token="+token)
+ makeRequest(t, req, expected)
+}
+
+func testViewGPGKeys(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ req := NewRequest(t, "GET", "/api/v1/users/user2/gpg_keys?token="+token)
+ makeRequest(t, req, expected)
+}
+
+func testGetGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ req := NewRequest(t, "GET", "/api/v1/user/gpg_keys/1?token="+token)
+ makeRequest(t, req, expected)
+}
+
+func testDeleteGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ req := NewRequest(t, "DELETE", "/api/v1/user/gpg_keys/1?token="+token)
+ makeRequest(t, req, expected)
+}
+
+func testCreateGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int, publicKey string) {
+ req := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys?token="+token, api.CreateGPGKeyOption{
+ ArmoredKey: publicKey,
+ })
+ makeRequest(t, req, expected)
+}
+
+func testCreateInvalidGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ testCreateGPGKey(t, makeRequest, token, expected, "invalid_key")
+}
+
+func testCreateNoneRegistredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFmGUygBCACjCNbKvMGgp0fd5vyFW9olE1CLCSyyF9gQN2hSuzmZLuAZF2Kh
+dCMCG2T1UwzUB/yWUFWJ2BtCwSjuaRv+cGohqEy6bhEBV90peGA33lHfjx7wP25O
+7moAphDOTZtDj1AZfCh/PTcJut8Lc0eRDMhNyp/bYtO7SHNT1Hr6rrCV/xEtSAvR
+3b148/tmIBiSadaLwc558KU3ucjnW5RVGins3AjBZ+TuT4XXVH/oeLSeXPSJ5rt1
+rHwaseslMqZ4AbvwFLx5qn1OC9rEQv/F548QsA8m0IntLjoPon+6wcubA9Gra21c
+Fp6aRYl9x7fiqXDLg8i3s2nKdV7+e6as6Tp9ABEBAAG0FG5vdGtub3duQGV4YW1w
+bGUuY29tiQEcBBABAgAGBQJZhlMoAAoJEC8+pvYULDtte/wH/2JNrhmHwDY+hMj0
+batIK4HICnkKxjIgbha80P2Ao08NkzSge58fsxiKDFYAQjHui+ZAw4dq79Ax9AOO
+Iv2GS9+DUfWhrb6RF+vNuJldFzcI0rTW/z2q+XGKrUCwN3khJY5XngHfQQrdBtMK
+qsoUXz/5B8g422RTbo/SdPsyYAV6HeLLeV3rdgjI1fpaW0seZKHeTXQb/HvNeuPg
+qz+XV1g6Gdqa1RjDOaX7A8elVKxrYq3LBtc93FW+grBde8n7JL0zPM3DY+vJ0IJZ
+INx/MmBfmtCq05FqNclvU+sj2R3N1JJOtBOjZrJHQbJhzoILou8AkxeX1A+q9OAz
+1geiY5E=
+=TkP3
+-----END PGP PUBLIC KEY BLOCK-----`)
+}
+
+func testCreateValidGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ // User2 <user2@example.com> //primary & activated
+ testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFmGVsMBCACuxgZ7W7rI9xN08Y4M7B8yx/6/I4Slm94+wXf8YNRvAyqj30dW
+VJhyBcnfNRDLKSQp5o/hhfDkCgdqBjLa1PnHlGS3PXJc0hP/FyYPD2BFvNMPpCYS
+eu3T1qKSNXm6X0XOWD2LIrdiDC8HaI9FqZVMI/srMK2CF8XCL2m67W1FuoPlWzod
+5ORy0IZB7spoF0xihmcgnEGElRmdo5w/vkGH8U7Zyn9Eb57UVFeafgeskf4wqB23
+BjbMdW2YaB+yzMRwYgOnD5lnBD4uqSmvjaV9C0kxn7x+oJkkiRV8/z1cNcO+BaeQ
+Akh/yTTeTzYGSc/ZOqCX1O+NOPgSeixVlqenABEBAAG0GVVzZXIyIDx1c2VyMkBl
+eGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRXgbSh0TtGbgRd7XI46jvO1zKYLAUCWYZW
+wwIbAwUJA8JnAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRA46jvO1zKYLF/e
+B/91wm2KLMIQBZBA9WA2/+9rQWTo9EqgYrXN60rEzX3cYJWXZiE4DrKR1oWDGNLi
+KXOCW62snvJldolBqq0ZqaKvPKzl0Y5TRqbYEc9AjUSqgRin1b+G2DevLGT4ibq+
+7ocQvz0XkASEUAgHahp0Ubiiib1521WwT/duL+AG8Gg0+DK09RfV3eX5/EOkQCKv
+8cutqgsd2Smz40A8wXuJkRcipZBtrB/GkUaZ/eJdwEeSYZjEA9GWF61LJT2stvRN
+HCk7C3z3pVEek1PluiFs/4VN8BG8yDzW4c0tLty4Fj3VwPqwIbB5AJbquVfhQCb4
+Eep2lm3Lc9b1OwO5N3coPJkouQENBFmGVsMBCADAGba2L6NCOE1i3WIP6CPzbdOo
+N3gdTfTgccAx9fNeon9jor+3tgEjlo9/6cXiRoksOV6W4wFab/ZwWgwN6JO4CGvZ
+Wi7EQwMMMp1E36YTojKQJrcA9UvMnTHulqQQ88F5E845DhzFQM3erv42QZZMBAX3
+kXCgy1GNFocl6tLUvJdEqs+VcJGGANMpmzE4WLa8KhSYnxipwuQ62JBy9R+cHyKT
+OARk8znRqSu5bT3LtlrZ/HXu+6Oy4+2uCdNzZIh5J5tPS7CPA6ptl88iGVBte/CJ
+7cjgJWSQqeYp2Y5QvsWAivkQ4Ww9plHbbwV0A2eaHsjjWzlUl3HoJ/snMOhBABEB
+AAGJATwEGAEIACYWIQRXgbSh0TtGbgRd7XI46jvO1zKYLAUCWYZWwwIbDAUJA8Jn
+AAAKCRA46jvO1zKYLBwLCACQOpeRVrwIKVaWcPMYjVHHJsGscaLKpgpARAUgbiG6
+Cbc2WI8Sm3fRwrY0VAfN+u9QwrtvxANcyB3vTgTzw7FimfhOimxiTSO8HQCfjDZF
+Xly8rq+Fua7+ClWUpy21IekW41VvZYjH2sL6EVP+UcEOaGAyN53XfhaRVZPhNtZN
+NKAE9N5EG3rbsZ33LzJj40rEKlzFSseAAPft8qA3IXjzFBx+PQXHMpNCagL79he6
+lqockTJ+oPmta4CF/J0U5LUr1tOZXheL3TP6m8d08gDrtn0YuGOPk87i9sJz+jR9
+uy6MA3VSB99SK9ducGmE1Jv8mcziREroz2TEGr0zPs6h
+=J59D
+-----END PGP PUBLIC KEY BLOCK-----`)
+}
+
+func testCreateValidSecondaryEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
+ // User2 <user2-2@example.com> //secondary and not activated
+ testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBGC2K2cBDAC1+Xgk+8UfhASVgRngQi4rnQ8k0t+bWsBz4Czd26+cxVDRwlTT
+8PALdrbrY/e9iXjcVcZ8Npo4UYe7/LfnL57dc7tgbenRGYYrWyVoNNv58BVw4xCY
+RmgvdHWIIPGuz3aME0smHxbJ2KewYTqjTPuVKF/wrHTwCpVWdjYKC5KDo3yx0mro
+xf9vOJOnkWNMiEw7TiZfkrbUqxyA53BVsSNKRX5C3b4FJcVT7eiAq7sDAaFxjEHy
+ahZslmvg7XZxWzSVzxDNesR7f4xuop8HBjzaluJoVuwiyWculTvz1b6hyHVQr+ad
+h8JGjj1tySI65OTFsTuptsfHXjtjl/NR4P6BXkf+FVwweaTQaEzpHkv0m9b9pY43
+CY/8XtS4uNPermiLG/Z0BB1eOCdoOQVHpjOa55IXQWhxXB6NZVyowiUbrR7jLDQy
+5JP7D1HmErTR8JRm3VDqGbSaCgugRgFX+lb/fpgFp9k02OeK+JQudolZOt1mVk+T
+C4xmEWxfiH15/JMAEQEAAbQbdXNlcjIgPHVzZXIyLTJAZXhhbXBsZS5jb20+iQHU
+BBMBCAA+FiEEB/Y4DM3Ba2H9iXmlPO9G70C+/D4FAmC2K2cCGwMFCQPCZwAFCwkI
+BwIGFQoJCAsCBBYCAwECHgECF4AACgkQPO9G70C+/D59/Av/XZIhCH4X2FpxCO3d
+oCa+sbYkBL5xeUoPfAx5ThXzqL/tllO88TKTMEGZF3k5pocXWH0xmhqlvDTcdb0i
+W3O0CN8FLmuotU51c0JC1mt9zwJP9PeJNyqxrMm01Yzj55z/Dz3QHSTlDjrWTWjn
+YBqDf2HfdM177oydfSYmevZni1aDmBalWpFPRvqISCO7uFnvg1hJQ5mD/0qie663
+QJ8LAAANg32H9DyPnYi9wU62WX0DMUVTjKctT3cnYCbirjjJ7ZlCCm+cf61CRX1B
+E1Ng/Ef3ZcUfXWitZSjfET/pKEMSNjsQawFpZ/LPCBl+UPHzaTPAASeGJvcbZ3py
+wZQLQc1MCu2hmMBQ8zHQTdS2Pp0RISxCQLYvVQL6DrcJDNiSqn9p9RQt5c5r5Pjx
+80BIPcjj3glOVP7PYE2azQAkt6reEjhimwCfjeDpiPnkBTY7Av2jCcUFhhemDY/j
+TRXK1paLphhJ36zC22SeHGxNNakjjuUakqB85DEUeoWuVm6ouQGNBGC2K2cBDADx
+G2rIAgMjdPtofhkEZXwv6zdNwmYOlIIM+59bam9Ep/vFq8F5f+xldevm5dvM8SeR
+pNwDGSOUf5OKBWBdsJFhlYBl7+EcKd/Tent/XS6JoA9ffF33b+r04L543+ykiKON
+WYeYi0F4WwYTIQgqZHJze1sPVkYGR5F0bL8PAcLuwd5dzZVi/q2HakrGdg29N8oY
+b/XnoR7FflPrNYdzO6hawi5Inx7KS7aWa0ZkARb0F4HSct+/m6nAZVsoJINLudyQ
+ut2NWeU8rWIm1hqyIxQFvuQJy46umq++10J/sWA98bkg41Rx+72+eP7DM5v8IgUp
+clJsfljRXIBWbmRAVZvtNI7PX9fwMMhf4M7wHO7G2WV39o1exKps5xFFcn8PUQiX
+jCSR81M145CgCdmLUR1y0pdkN/WIqjXBhkPIvO2dxEcodMNHb1aUUuUOnww6+xIP
+8rGVw+a2DUiALc8Qr5RP21AYKRctfiwhSQh2KODveMtyLI3U9C/eLRPp+QM3XB8A
+EQEAAYkBvAQYAQgAJhYhBAf2OAzNwWth/Yl5pTzvRu9Avvw+BQJgtitnAhsMBQkD
+wmcAAAoJEDzvRu9Avvw+3FcMAJBwupyJ4zwQFxTJ5BkDlusG3U2FXEf3bDrXhvNd
+qi8eS8Vo/vRiH/w/my5JFpz1o2tJToryF71D+uF5DTItalKquhsQ9reAEmXggqOh
+9Jd9mWJIEEWcRORiLNDKENKvE8bouw4U4hRaSF0IaGzAe5mO+oOvwal8L97wFxrZ
+4leM1GzkopiuNfbkkBBw2KJcMjYBHzzXSCALnVwhjbgkBEWPIg38APT3cr9KfnMM
+q8+tvsGLj4piAl3Lww7+GhSsDOUXH8btR41BSAQDrbO5q6oi/h4nuxoNmQIDW/Ug
+s+dd5hnY2FtHRjb4FCR9kAjdTE6stc8wzohWfbg1N+12TTA2ylByAumICVXixavH
+RJ7l0OiWJk388qw9mqh3k8HcBxL7OfDlFC9oPmCS0iYiIwW/Yc80kBhoxcvl/Xa7
+mIMMn8taHIaQO7v9ln2EVQYTzbNCmwTw9ovTM0j/Pbkg2EftfP1TCoxQHvBnsCED
+6qgtsUdi5eviONRkBgeZtN3oxA==
+=MgDv
+-----END PGP PUBLIC KEY BLOCK-----`)
+}
diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go
new file mode 100644
index 0000000000..5a798f79f0
--- /dev/null
+++ b/tests/integration/api_helper_for_declarative_test.go
@@ -0,0 +1,464 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/queue"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/forms"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type APITestContext struct {
+ Reponame string
+ Session *TestSession
+ Token string
+ Username string
+ ExpectedCode int
+}
+
+func NewAPITestContext(t *testing.T, username, reponame string) APITestContext {
+ session := loginUser(t, username)
+ token := getTokenForLoggedInUser(t, session)
+ return APITestContext{
+ Session: session,
+ Token: token,
+ Username: username,
+ Reponame: reponame,
+ }
+}
+
+func (ctx APITestContext) GitPath() string {
+ return fmt.Sprintf("%s/%s.git", ctx.Username, ctx.Reponame)
+}
+
+func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
+ return func(t *testing.T) {
+ createRepoOption := &api.CreateRepoOption{
+ AutoInit: !empty,
+ Description: "Temporary repo",
+ Name: ctx.Reponame,
+ Private: true,
+ Template: true,
+ Gitignores: "",
+ License: "WTFPL",
+ Readme: "Default",
+ }
+ req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+ctx.Token, createRepoOption)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
+
+ var repository api.Repository
+ DecodeJSON(t, resp, &repository)
+ if len(callback) > 0 {
+ callback[0](t, repository)
+ }
+ }
+}
+
+func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
+ return func(t *testing.T) {
+ req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), ctx.Token), editRepoOption)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+ var repository api.Repository
+ DecodeJSON(t, resp, &repository)
+ if len(callback) > 0 {
+ callback[0](t, repository)
+ }
+ }
+}
+
+func doAPIAddCollaborator(ctx APITestContext, username string, mode perm.AccessMode) func(*testing.T) {
+ return func(t *testing.T) {
+ permission := "read"
+
+ if mode == perm.AccessModeAdmin {
+ permission = "admin"
+ } else if mode > perm.AccessModeRead {
+ permission = "write"
+ }
+ addCollaboratorOption := &api.AddCollaboratorOption{
+ Permission: &permission,
+ }
+ req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/collaborators/%s?token=%s", ctx.Username, ctx.Reponame, username, ctx.Token), addCollaboratorOption)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusNoContent)
+ }
+}
+
+func doAPIForkRepository(ctx APITestContext, username string, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
+ return func(t *testing.T) {
+ createForkOption := &api.CreateForkOption{}
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks?token=%s", username, ctx.Reponame, ctx.Token), createForkOption)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusAccepted)
+ var repository api.Repository
+ DecodeJSON(t, resp, &repository)
+ if len(callback) > 0 {
+ callback[0](t, repository)
+ }
+ }
+}
+
+func doAPIGetRepository(ctx APITestContext, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
+
+ req := NewRequest(t, "GET", urlStr)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+ var repository api.Repository
+ DecodeJSON(t, resp, &repository)
+ if len(callback) > 0 {
+ callback[0](t, repository)
+ }
+ }
+}
+
+func doAPIDeleteRepository(ctx APITestContext) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
+
+ req := NewRequest(t, "DELETE", urlStr)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusNoContent)
+ }
+}
+
+func doAPICreateUserKey(ctx APITestContext, keyname, keyFile string, callback ...func(*testing.T, api.PublicKey)) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/user/keys?token=%s", ctx.Token)
+
+ dataPubKey, err := os.ReadFile(keyFile + ".pub")
+ assert.NoError(t, err)
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateKeyOption{
+ Title: keyname,
+ Key: string(dataPubKey),
+ })
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
+ var publicKey api.PublicKey
+ DecodeJSON(t, resp, &publicKey)
+ if len(callback) > 0 {
+ callback[0](t, publicKey)
+ }
+ }
+}
+
+func doAPIDeleteUserKey(ctx APITestContext, keyID int64) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/user/keys/%d?token=%s", keyID, ctx.Token)
+
+ req := NewRequest(t, "DELETE", urlStr)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusNoContent)
+ }
+}
+
+func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly bool) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", ctx.Username, ctx.Reponame, ctx.Token)
+
+ dataPubKey, err := os.ReadFile(keyFile + ".pub")
+ assert.NoError(t, err)
+ req := NewRequestWithJSON(t, "POST", urlStr, api.CreateKeyOption{
+ Title: keyname,
+ Key: string(dataPubKey),
+ ReadOnly: readOnly,
+ })
+
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusCreated)
+ }
+}
+
+func doAPICreatePullRequest(ctx APITestContext, owner, repo, baseBranch, headBranch string) func(*testing.T) (api.PullRequest, error) {
+ return func(t *testing.T) (api.PullRequest, error) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s",
+ owner, repo, ctx.Token)
+ req := NewRequestWithJSON(t, http.MethodPost, urlStr, &api.CreatePullRequestOption{
+ Head: headBranch,
+ Base: baseBranch,
+ Title: fmt.Sprintf("create a pr from %s to %s", headBranch, baseBranch),
+ })
+
+ expected := http.StatusCreated
+ if ctx.ExpectedCode != 0 {
+ expected = ctx.ExpectedCode
+ }
+ resp := ctx.Session.MakeRequest(t, req, expected)
+
+ decoder := json.NewDecoder(resp.Body)
+ pr := api.PullRequest{}
+ err := decoder.Decode(&pr)
+ return pr, err
+ }
+}
+
+func doAPIGetPullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) (api.PullRequest, error) {
+ return func(t *testing.T) (api.PullRequest, error) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d?token=%s",
+ owner, repo, index, ctx.Token)
+ req := NewRequest(t, http.MethodGet, urlStr)
+
+ expected := http.StatusOK
+ if ctx.ExpectedCode != 0 {
+ expected = ctx.ExpectedCode
+ }
+ resp := ctx.Session.MakeRequest(t, req, expected)
+
+ decoder := json.NewDecoder(resp.Body)
+ pr := api.PullRequest{}
+ err := decoder.Decode(&pr)
+ return pr, err
+ }
+}
+
+func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
+ owner, repo, index, ctx.Token)
+
+ var req *http.Request
+ var resp *httptest.ResponseRecorder
+
+ for i := 0; i < 6; i++ {
+ req = NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
+ MergeMessageField: "doAPIMergePullRequest Merge",
+ Do: string(repo_model.MergeStyleMerge),
+ })
+
+ resp = ctx.Session.MakeRequest(t, req, NoExpectedStatus)
+
+ if resp.Code != http.StatusMethodNotAllowed {
+ break
+ }
+ err := api.APIError{}
+ DecodeJSON(t, resp, &err)
+ assert.EqualValues(t, "Please try again later", err.Message)
+ queue.GetManager().FlushAll(context.Background(), 5*time.Second)
+ <-time.After(1 * time.Second)
+ }
+
+ expected := ctx.ExpectedCode
+ if expected == 0 {
+ expected = http.StatusOK
+ }
+
+ if !assert.EqualValues(t, expected, resp.Code,
+ "Request: %s %s", req.Method, req.URL.String()) {
+ logUnexpectedResponse(t, resp)
+ }
+ }
+}
+
+func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID string, index int64) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
+ owner, repo, index, ctx.Token)
+ req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
+ Do: string(repo_model.MergeStyleManuallyMerged),
+ MergeCommitID: commitID,
+ })
+
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusOK)
+ }
+}
+
+func doAPIAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
+ owner, repo, index, ctx.Token)
+ req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
+ MergeMessageField: "doAPIMergePullRequest Merge",
+ Do: string(repo_model.MergeStyleMerge),
+ MergeWhenChecksSucceed: true,
+ })
+
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, 200)
+ }
+}
+
+func doAPICancelAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
+ return func(t *testing.T) {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s",
+ owner, repo, index, ctx.Token)
+ req := NewRequest(t, http.MethodDelete, urlStr)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, 204)
+ }
+}
+
+func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
+ return func(t *testing.T) {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+ var branch api.Branch
+ DecodeJSON(t, resp, &branch)
+ if len(callback) > 0 {
+ callback[0](t, branch)
+ }
+ }
+}
+
+func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
+ return func(t *testing.T) {
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", ctx.Username, ctx.Reponame, treepath, ctx.Token)
+ req := NewRequestWithJSON(t, "POST", url, &options)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
+
+ var contents api.FileResponse
+ DecodeJSON(t, resp, &contents)
+ if len(callback) > 0 {
+ callback[0](t, contents)
+ }
+ }
+}
+
+func doAPICreateOrganization(ctx APITestContext, options *api.CreateOrgOption, callback ...func(*testing.T, api.Organization)) func(t *testing.T) {
+ return func(t *testing.T) {
+ url := fmt.Sprintf("/api/v1/orgs?token=%s", ctx.Token)
+
+ req := NewRequestWithJSON(t, "POST", url, &options)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
+
+ var contents api.Organization
+ DecodeJSON(t, resp, &contents)
+ if len(callback) > 0 {
+ callback[0](t, contents)
+ }
+ }
+}
+
+func doAPICreateOrganizationRepository(ctx APITestContext, orgName string, options *api.CreateRepoOption, callback ...func(*testing.T, api.Repository)) func(t *testing.T) {
+ return func(t *testing.T) {
+ url := fmt.Sprintf("/api/v1/orgs/%s/repos?token=%s", orgName, ctx.Token)
+
+ req := NewRequestWithJSON(t, "POST", url, &options)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
+
+ var contents api.Repository
+ DecodeJSON(t, resp, &contents)
+ if len(callback) > 0 {
+ callback[0](t, contents)
+ }
+ }
+}
+
+func doAPICreateOrganizationTeam(ctx APITestContext, orgName string, options *api.CreateTeamOption, callback ...func(*testing.T, api.Team)) func(t *testing.T) {
+ return func(t *testing.T) {
+ url := fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", orgName, ctx.Token)
+
+ req := NewRequestWithJSON(t, "POST", url, &options)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
+
+ var contents api.Team
+ DecodeJSON(t, resp, &contents)
+ if len(callback) > 0 {
+ callback[0](t, contents)
+ }
+ }
+}
+
+func doAPIAddUserToOrganizationTeam(ctx APITestContext, teamID int64, username string) func(t *testing.T) {
+ return func(t *testing.T) {
+ url := fmt.Sprintf("/api/v1/teams/%d/members/%s?token=%s", teamID, username, ctx.Token)
+
+ req := NewRequest(t, "PUT", url)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusNoContent)
+ }
+}
+
+func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, repoName string) func(t *testing.T) {
+ return func(t *testing.T) {
+ url := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s?token=%s", teamID, orgName, repoName, ctx.Token)
+
+ req := NewRequest(t, "PUT", url)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusNoContent)
+ }
+}
diff --git a/tests/integration/api_httpsig_test.go b/tests/integration/api_httpsig_test.go
new file mode 100644
index 0000000000..80b3c586b4
--- /dev/null
+++ b/tests/integration/api_httpsig_test.go
@@ -0,0 +1,138 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/go-fed/httpsig"
+ "golang.org/x/crypto/ssh"
+)
+
+const (
+ httpsigPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAQEAqjmQeb5Eb1xV7qbNf9ErQ0XRvKZWzUsLFhJzZz+Ab7q8WtPs91vQ
+fBiypw4i8OTG6WzDcgZaV8Ndxn7iHnIstdA1k89MVG4stydymmwmk9+mrCMNsu5OmdIy9F
+AZ61RDcKuf5VG2WKkmeK0VO+OMJIYfE1C6czNeJ6UAmcIOmhGxvjMI83XUO9n0ftwTwayp
++XU5prvKx/fTvlPjbraPNU4OzwPjVLqXBzpoXYhBquPaZYFRVyvfFZLObYsmy+BrsxcloM
+l+9w4P0ATJ9njB7dRDL+RrN4uhhYSihqOK4w4vaiOj1+aA0eC0zXunEfLXfGIVQ/FhWcCy
+5f72mMiKnQAAA9AxSmzFMUpsxQAAAAdzc2gtcnNhAAABAQCqOZB5vkRvXFXups1/0StDRd
+G8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xU
+biy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQ
+CZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq
+49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6
+PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqdAAAAAwEAAQAAAQBz+nyBNi2SYir6SxPA
+flcnoq5gBkUl4ndPNosCUbXEakpi5/mQHzJRGtK+F1efIYCVEdGoIsPy/90onNKbQ9dKmO
+2oI5kx/U7iCzJ+HCm8nqkEp21x+AP9scWdx+Wg/OxmG8j5iU7f4X+gwOyyvTqCuA78Lgia
+7Oi9wiJCoIEqXr6dRYGJzfASwKA2dj995HzATexleLSD5fQCmZTF+Vh5OQ5WmE+c53JdZS
+T3Plie/P/smgSWBtf1fWr6JL2+EBsqQsIK1Jo7r/7rxsz+ILoVfnneNQY4QSa9W+t6ZAI+
+caSA0Guv7vC92ewjlMVlwKa3XaEjMJb5sFlg1r6TYMwBAAAAgQDQwXvgSXNaSHIeH53/Ab
+t4BlNibtxK8vY8CZFloAKXkjrivKSlDAmQCM0twXOweX2ScPjE+XlSMV4AUsv/J6XHGHci
+W3+PGIBfc/fQRBpiyhzkoXYDVrlkSKHffCnAqTUQlYkhr0s7NkZpEeqPE0doAUs4dK3Iqb
+zdtz8e5BPXZwAAAIEA4U/JskIu5Oge8Is2OLOhlol0EJGw5JGodpFyhbMC+QYK9nYqy7wI
+a6mZ2EfOjjwIZD/+wYyulw6cRve4zXwgzUEXLIKp8/H3sYvJK2UMeP7y68sQFqGxbm6Rnh
+tyBBSaJQnOXVOFf9gqZGCyO/J0Illg3AXTuC8KS/cxwasC38EAAACBAMFo/6XQoR6E3ynj
+VBaz2SilWqQBixUyvcNz8LY73IIDCecoccRMFSEKhWtvlJijxvFbF9M8g9oKAVPuub4V5r
+CGmwVPEd5yt4C2iyV0PhLp1PA2/i42FpCSnHaz/EXSz6ncTZcOMMuDqUbgUUpQg4VSUDl9
+fhTNAzWwZoQ91aHdAAAAFHUwMDIyMTQ2QGljdHMtcC1ueC03AQIDBAUG
+-----END OPENSSH PRIVATE KEY-----`
+ httpsigCertificate = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgiR7SU8gmZLhopx4Y03nOXVuAb+4fyMcJYjMGcE1Z2oEAAAADAQABAAABAQCqOZB5vkRvXFXups1/0StDRdG8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xUbiy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQCZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqdAAAAAAAAAAEAAAABAAAABXVzZXIxAAAACQAAAAV1c2VyMQAAAABimoIOAAAAAMCWkRMAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABlwAAAAdzc2gtcnNhAAAAAwEAAQAAAYEAm+AwtXTBZyeqV1qOxjMU3Ibc5iR2M3zerGfRQDxUeIozC3xpIvqJbzjDuRapdf8hpxn2xC0GtUusuLIUr4/+Svs1BUnJhF2H9xnK/O0aopS5MpNekUvnBzQdbvO8Ux2xE2mt58giXhkEaXeCEODSqG++OZsA2e40AR/AGRJ4OdDofMvH4vLJAQQc2mKdYpYL8xu+NC+7nsenx1etpsqtEl3gmvqCVI6t9uhVPMvlbGt9h/AN3u7ToF2T3bdk1TZbcdkvR9ljvETIuy32ksAETX8tc7vm30edK+nn/GMeWCgjM+MFm9Uh1NRkvNNJozo5SJy0DkWETTJUsEdfry5VQ3IjqhWqQ0m4/mDlTmsEdEdWqpUiqWZLd9w7jgT8fanuglZyIu2fj8fyqjPjiws5S2P0Uvi28UKQ1nH01UYj/kuakU3BNzN1IqDf3tARP9fjKV/dCBqb1ZAOtyC2GyhGuGzNwEi+woUwq+sTeV0/hqVSb3hSitXHzcfRMRyOK82BAAABlAAAAAxyc2Etc2hhMi01MTIAAAGAMBfgZFvz4BdxriGKYd6eRhMo6hf+I8S9uzNRsflJXHuA+HR9ExIm/Q9JjKmfThQzNyGGBOBILaDU205SAJuG+kk3SieSQDd75ZQd8YmNlCc+516AriOsTiyVCupnf3I2euTjMZqEZbJcBbkBljppTOWQVN7xxE8QakDfGhg0+RjJE9wYOTmkKpDBfII5Nw8V5DoOD7kNEpXYqHdy/8lVxpqUYNIP1J0dNP4f6qBcZcM1PDA12q8zwIGqSNNjf2UXY/Nr8nv9CnK4fB8NDOPKTBa4cm48BGbvM/X0l6dYKswuZ9Np8lw+y6+GxTgznGCrkzMmuEV4FzSq4xHp41H2L2MTwUkwYaeyG1VP6aWkvn6zPkSxaaJDfQX7CAFe17IhIGXR0UPLjKjh35nDLzMWb/W6/W1lK9YkZNHXSf7Z9m9MUAZN7yQgOggGsuYEW4imZxvZizMd+fdDu9mbhr0FDis89I7MSJDnyYRE9FXS7p3QpppBwGcss/9yV3JV3Bjc`
+)
+
+func TestHTTPSigPubKey(t *testing.T) {
+ // Add our public key to user1
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+ token := url.QueryEscape(getTokenForLoggedInUser(t, session))
+ keysURL := fmt.Sprintf("/api/v1/user/keys?token=%s", token)
+ keyType := "ssh-rsa"
+ keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqOZB5vkRvXFXups1/0StDRdG8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xUbiy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQCZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqd"
+ rawKeyBody := api.CreateKeyOption{
+ Title: "test-key",
+ Key: keyType + " " + keyContent,
+ }
+ req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody)
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ // parse our private key and create the httpsig request
+ sshSigner, _ := ssh.ParsePrivateKey([]byte(httpsigPrivateKey))
+ keyID := ssh.FingerprintSHA256(sshSigner.PublicKey())
+
+ // create the request
+ req = NewRequest(t, "GET", "/api/v1/admin/users")
+
+ signer, _, err := httpsig.NewSSHSigner(sshSigner, httpsig.DigestSha512, []string{httpsig.RequestTarget, "(created)", "(expires)"}, httpsig.Signature, 10)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // sign the request
+ err = signer.SignRequest(keyID, req, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // make the request
+ MakeRequest(t, req, http.StatusOK)
+}
+
+func TestHTTPSigCert(t *testing.T) {
+ // Add our public key to user1
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+
+ csrf := GetCSRF(t, session, "/user/settings/keys")
+ req := NewRequestWithValues(t, "POST", "/user/settings/keys", map[string]string{
+ "_csrf": csrf,
+ "content": "user1",
+ "title": "principal",
+ "type": "principal",
+ })
+
+ session.MakeRequest(t, req, http.StatusSeeOther)
+ pkcert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(httpsigCertificate))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // parse our private key and create the httpsig request
+ sshSigner, _ := ssh.ParsePrivateKey([]byte(httpsigPrivateKey))
+ keyID := "gitea"
+
+ // create our certificate signer using the ssh signer and our certificate
+ certSigner, err := ssh.NewCertSigner(pkcert.(*ssh.Certificate), sshSigner)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create the request
+ req = NewRequest(t, "GET", "/api/v1/admin/users")
+
+ // add our cert to the request
+ certString := base64.RawStdEncoding.EncodeToString(pkcert.(*ssh.Certificate).Marshal())
+ req.Header.Add("x-ssh-certificate", certString)
+
+ signer, _, err := httpsig.NewSSHSigner(certSigner, httpsig.DigestSha512, []string{httpsig.RequestTarget, "(created)", "(expires)", "x-ssh-certificate"}, httpsig.Signature, 10)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // sign the request
+ err = signer.SignRequest(keyID, req, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // make the request
+ MakeRequest(t, req, http.StatusOK)
+}
diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go
new file mode 100644
index 0000000000..586c50a55f
--- /dev/null
+++ b/tests/integration/api_issue_label_test.go
@@ -0,0 +1,208 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIModifyLabels(t *testing.T) {
+ assert.NoError(t, unittest.LoadFixtures())
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels?token=%s", owner.Name, repo.Name, token)
+
+ // CreateLabel
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "TestL 1",
+ Color: "abcdef",
+ Description: "test label",
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ apiLabel := new(api.Label)
+ DecodeJSON(t, resp, &apiLabel)
+ dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, RepoID: repo.ID})
+ assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
+ assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
+
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "TestL 2",
+ Color: "#123456",
+ Description: "jet another test label",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "WrongTestL",
+ Color: "#12345g",
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // ListLabels
+ req = NewRequest(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiLabels []*api.Label
+ DecodeJSON(t, resp, &apiLabels)
+ assert.Len(t, apiLabels, 2)
+
+ // GetLabel
+ singleURLStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d?token=%s", owner.Name, repo.Name, dbLabel.ID, token)
+ req = NewRequest(t, "GET", singleURLStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiLabel)
+ assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
+
+ // EditLabel
+ newName := "LabelNewName"
+ newColor := "09876a"
+ newColorWrong := "09g76a"
+ req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
+ Name: &newName,
+ Color: &newColor,
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiLabel)
+ assert.EqualValues(t, newColor, apiLabel.Color)
+ req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
+ Color: &newColorWrong,
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // DeleteLabel
+ req = NewRequest(t, "DELETE", singleURLStr)
+ session.MakeRequest(t, req, http.StatusNoContent)
+}
+
+func TestAPIAddIssueLabels(t *testing.T) {
+ assert.NoError(t, unittest.LoadFixtures())
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
+ _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID, ID: 2})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
+ repo.OwnerName, repo.Name, issue.Index, token)
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
+ Labels: []int64{1, 2},
+ })
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiLabels []*api.Label
+ DecodeJSON(t, resp, &apiLabels)
+ assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
+
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2})
+}
+
+func TestAPIReplaceIssueLabels(t *testing.T) {
+ assert.NoError(t, unittest.LoadFixtures())
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
+ label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
+ owner.Name, repo.Name, issue.Index, token)
+ req := NewRequestWithJSON(t, "PUT", urlStr, &api.IssueLabelsOption{
+ Labels: []int64{label.ID},
+ })
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiLabels []*api.Label
+ DecodeJSON(t, resp, &apiLabels)
+ if assert.Len(t, apiLabels, 1) {
+ assert.EqualValues(t, label.ID, apiLabels[0].ID)
+ }
+
+ unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issue.ID}, 1)
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
+}
+
+func TestAPIModifyOrgLabels(t *testing.T) {
+ assert.NoError(t, unittest.LoadFixtures())
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ user := "user1"
+ session := loginUser(t, user)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/orgs/%s/labels?token=%s", owner.Name, token)
+
+ // CreateLabel
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "TestL 1",
+ Color: "abcdef",
+ Description: "test label",
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ apiLabel := new(api.Label)
+ DecodeJSON(t, resp, &apiLabel)
+ dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, OrgID: owner.ID})
+ assert.EqualValues(t, dbLabel.Name, apiLabel.Name)
+ assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
+
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "TestL 2",
+ Color: "#123456",
+ Description: "jet another test label",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
+ Name: "WrongTestL",
+ Color: "#12345g",
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // ListLabels
+ req = NewRequest(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiLabels []*api.Label
+ DecodeJSON(t, resp, &apiLabels)
+ assert.Len(t, apiLabels, 4)
+
+ // GetLabel
+ singleURLStr := fmt.Sprintf("/api/v1/orgs/%s/labels/%d?token=%s", owner.Name, dbLabel.ID, token)
+ req = NewRequest(t, "GET", singleURLStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiLabel)
+ assert.EqualValues(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
+
+ // EditLabel
+ newName := "LabelNewName"
+ newColor := "09876a"
+ newColorWrong := "09g76a"
+ req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
+ Name: &newName,
+ Color: &newColor,
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiLabel)
+ assert.EqualValues(t, newColor, apiLabel.Color)
+ req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
+ Color: &newColorWrong,
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // DeleteLabel
+ req = NewRequest(t, "DELETE", singleURLStr)
+ session.MakeRequest(t, req, http.StatusNoContent)
+}
diff --git a/tests/integration/api_issue_milestone_test.go b/tests/integration/api_issue_milestone_test.go
new file mode 100644
index 0000000000..e22a091bb8
--- /dev/null
+++ b/tests/integration/api_issue_milestone_test.go
@@ -0,0 +1,81 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIIssuesMilestone(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: milestone.RepoID})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ assert.Equal(t, int64(1), int64(milestone.NumIssues))
+ assert.Equal(t, structs.StateOpen, milestone.State())
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // update values of issue
+ milestoneState := "closed"
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d?token=%s", owner.Name, repo.Name, milestone.ID, token)
+ req := NewRequestWithJSON(t, "PATCH", urlStr, structs.EditMilestoneOption{
+ State: &milestoneState,
+ })
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiMilestone structs.Milestone
+ DecodeJSON(t, resp, &apiMilestone)
+ assert.EqualValues(t, "closed", apiMilestone.State)
+
+ req = NewRequest(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiMilestone2 structs.Milestone
+ DecodeJSON(t, resp, &apiMilestone2)
+ assert.EqualValues(t, "closed", apiMilestone2.State)
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?token=%s", owner.Name, repo.Name, token), structs.CreateMilestoneOption{
+ Title: "wow",
+ Description: "closed one",
+ State: "closed",
+ })
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, &apiMilestone)
+ assert.Equal(t, "wow", apiMilestone.Title)
+ assert.Equal(t, structs.StateClosed, apiMilestone.State)
+
+ var apiMilestones []structs.Milestone
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s&token=%s", owner.Name, repo.Name, "all", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiMilestones)
+ assert.Len(t, apiMilestones, 4)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%s?token=%s", owner.Name, repo.Name, apiMilestones[2].Title, token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiMilestone)
+ assert.EqualValues(t, apiMilestones[2], apiMilestone)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s&name=%s&token=%s", owner.Name, repo.Name, "all", "milestone2", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiMilestones)
+ assert.Len(t, apiMilestones, 1)
+ assert.Equal(t, int64(2), apiMilestones[0].ID)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d?token=%s", owner.Name, repo.Name, apiMilestone.ID, token))
+ session.MakeRequest(t, req, http.StatusNoContent)
+}
diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go
new file mode 100644
index 0000000000..a3cb9303fb
--- /dev/null
+++ b/tests/integration/api_issue_reaction_test.go
@@ -0,0 +1,144 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/convert"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIIssuesReactions(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+ _ = issue.LoadRepo(db.DefaultContext)
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions?token=%s",
+ owner.Name, issue.Repo.Name, issue.Index, token)
+
+ // Try to add not allowed reaction
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "wrong",
+ })
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // Delete not allowed reaction
+ req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
+ Reaction: "zzz",
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Add allowed reaction
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "rocket",
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ var apiNewReaction api.Reaction
+ DecodeJSON(t, resp, &apiNewReaction)
+
+ // Add existing reaction
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // Get end result of reaction list of issue #1
+ req = NewRequestf(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiReactions []*api.Reaction
+ DecodeJSON(t, resp, &apiReactions)
+ expectResponse := make(map[int]api.Reaction)
+ expectResponse[0] = api.Reaction{
+ User: convert.ToUser(user2, user2),
+ Reaction: "eyes",
+ Created: time.Unix(1573248003, 0),
+ }
+ expectResponse[1] = apiNewReaction
+ assert.Len(t, apiReactions, 2)
+ for i, r := range apiReactions {
+ assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
+ assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
+ assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
+ }
+}
+
+func TestAPICommentReactions(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
+ _ = comment.LoadIssue()
+ issue := comment.Issue
+ _ = issue.LoadRepo(db.DefaultContext)
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s",
+ owner.Name, issue.Repo.Name, comment.ID, token)
+
+ // Try to add not allowed reaction
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "wrong",
+ })
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // Delete none existing reaction
+ req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
+ Reaction: "eyes",
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Add allowed reaction
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "+1",
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ var apiNewReaction api.Reaction
+ DecodeJSON(t, resp, &apiNewReaction)
+
+ // Add existing reaction
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // Get end result of reaction list of issue #1
+ req = NewRequestf(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiReactions []*api.Reaction
+ DecodeJSON(t, resp, &apiReactions)
+ expectResponse := make(map[int]api.Reaction)
+ expectResponse[0] = api.Reaction{
+ User: convert.ToUser(user2, user2),
+ Reaction: "laugh",
+ Created: time.Unix(1573248004, 0),
+ }
+ expectResponse[1] = api.Reaction{
+ User: convert.ToUser(user1, user1),
+ Reaction: "laugh",
+ Created: time.Unix(1573248005, 0),
+ }
+ expectResponse[2] = apiNewReaction
+ assert.Len(t, apiReactions, 3)
+ for i, r := range apiReactions {
+ assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
+ assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
+ assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
+ }
+}
diff --git a/tests/integration/api_issue_stopwatch_test.go b/tests/integration/api_issue_stopwatch_test.go
new file mode 100644
index 0000000000..c2ad9c45e8
--- /dev/null
+++ b/tests/integration/api_issue_stopwatch_test.go
@@ -0,0 +1,92 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIListStopWatches(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/user/stopwatches?token=%s", token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiWatches []*api.StopWatch
+ DecodeJSON(t, resp, &apiWatches)
+ stopwatch := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: owner.ID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: stopwatch.IssueID})
+ if assert.Len(t, apiWatches, 1) {
+ assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
+ assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
+ assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
+ assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
+ assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
+ assert.Greater(t, apiWatches[0].Seconds, int64(0))
+ }
+}
+
+func TestAPIStopStopWatches(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ _ = issue.LoadRepo(db.DefaultContext)
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/stop?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
+ session.MakeRequest(t, req, http.StatusCreated)
+ session.MakeRequest(t, req, http.StatusConflict)
+}
+
+func TestAPICancelStopWatches(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+ _ = issue.LoadRepo(db.DefaultContext)
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/stopwatch/delete?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ session.MakeRequest(t, req, http.StatusConflict)
+}
+
+func TestAPIStartStopWatches(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+ _ = issue.LoadRepo(db.DefaultContext)
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/start?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
+ session.MakeRequest(t, req, http.StatusCreated)
+ session.MakeRequest(t, req, http.StatusConflict)
+}
diff --git a/tests/integration/api_issue_subscription_test.go b/tests/integration/api_issue_subscription_test.go
new file mode 100644
index 0000000000..f4588fbbc4
--- /dev/null
+++ b/tests/integration/api_issue_subscription_test.go
@@ -0,0 +1,77 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIIssueSubscriptions(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
+ issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ issue3 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+ issue4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4})
+ issue5 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 8})
+
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue1.PosterID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ testSubscription := func(issue *issues_model.Issue, isWatching bool) {
+ issueRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check?token=%s", issueRepo.OwnerName, issueRepo.Name, issue.Index, token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ wi := new(api.WatchInfo)
+ DecodeJSON(t, resp, wi)
+
+ assert.EqualValues(t, isWatching, wi.Subscribed)
+ assert.EqualValues(t, !isWatching, wi.Ignored)
+ assert.EqualValues(t, issue.APIURL()+"/subscriptions", wi.URL)
+ assert.EqualValues(t, issue.CreatedUnix, wi.CreatedAt.Unix())
+ assert.EqualValues(t, issueRepo.APIURL(), wi.RepositoryURL)
+ }
+
+ testSubscription(issue1, true)
+ testSubscription(issue2, true)
+ testSubscription(issue3, true)
+ testSubscription(issue4, false)
+ testSubscription(issue5, false)
+
+ issue1Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue1.RepoID})
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue1Repo.OwnerName, issue1Repo.Name, issue1.Index, owner.Name, token)
+ req := NewRequest(t, "DELETE", urlStr)
+ session.MakeRequest(t, req, http.StatusCreated)
+ testSubscription(issue1, false)
+
+ req = NewRequest(t, "DELETE", urlStr)
+ session.MakeRequest(t, req, http.StatusOK)
+ testSubscription(issue1, false)
+
+ issue5Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue5.RepoID})
+ urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue5Repo.OwnerName, issue5Repo.Name, issue5.Index, owner.Name, token)
+ req = NewRequest(t, "PUT", urlStr)
+ session.MakeRequest(t, req, http.StatusCreated)
+ testSubscription(issue5, true)
+
+ req = NewRequest(t, "PUT", urlStr)
+ session.MakeRequest(t, req, http.StatusOK)
+ testSubscription(issue5, true)
+}
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
new file mode 100644
index 0000000000..3e651c620b
--- /dev/null
+++ b/tests/integration/api_issue_test.go
@@ -0,0 +1,329 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIListIssues(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name))
+
+ link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode()
+ resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ var apiIssues []*api.Issue
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}))
+ for _, apiIssue := range apiIssues {
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: apiIssue.ID, RepoID: repo.ID})
+ }
+
+ // test milestone filter
+ link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "type": {"all"}, "milestones": {"ignore,milestone1,3,4"}}.Encode()
+ resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ if assert.Len(t, apiIssues, 2) {
+ assert.EqualValues(t, 3, apiIssues[0].Milestone.ID)
+ assert.EqualValues(t, 1, apiIssues[1].Milestone.ID)
+ }
+
+ link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "created_by": {"user2"}}.Encode()
+ resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ if assert.Len(t, apiIssues, 1) {
+ assert.EqualValues(t, 5, apiIssues[0].ID)
+ }
+
+ link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "assigned_by": {"user1"}}.Encode()
+ resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ if assert.Len(t, apiIssues, 1) {
+ assert.EqualValues(t, 1, apiIssues[0].ID)
+ }
+
+ link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "mentioned_by": {"user4"}}.Encode()
+ resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ if assert.Len(t, apiIssues, 1) {
+ assert.EqualValues(t, 1, apiIssues[0].ID)
+ }
+}
+
+func TestAPICreateIssue(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ const body, title = "apiTestBody", "apiTestTitle"
+
+ repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
+ Body: body,
+ Title: title,
+ Assignee: owner.Name,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ var apiIssue api.Issue
+ DecodeJSON(t, resp, &apiIssue)
+ assert.Equal(t, body, apiIssue.Body)
+ assert.Equal(t, title, apiIssue.Title)
+
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
+ RepoID: repoBefore.ID,
+ AssigneeID: owner.ID,
+ Content: body,
+ Title: title,
+ })
+
+ repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues)
+ assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues)
+}
+
+func TestAPIEditIssue(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
+ repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
+ assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
+ assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
+ assert.Equal(t, api.StateOpen, issueBefore.State())
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // update values of issue
+ issueState := "closed"
+ removeDeadline := true
+ milestone := int64(4)
+ body := "new content!"
+ title := "new title from api set"
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
+ req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
+ State: &issueState,
+ RemoveDeadline: &removeDeadline,
+ Milestone: &milestone,
+ Body: &body,
+ Title: title,
+
+ // ToDo change more
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ var apiIssue api.Issue
+ DecodeJSON(t, resp, &apiIssue)
+
+ issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
+ repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
+
+ // check deleted user
+ assert.Equal(t, int64(500), issueAfter.PosterID)
+ assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext))
+ assert.Equal(t, int64(-1), issueAfter.PosterID)
+ assert.Equal(t, int64(-1), issueBefore.PosterID)
+ assert.Equal(t, int64(-1), apiIssue.Poster.ID)
+
+ // check repo change
+ assert.Equal(t, repoBefore.NumClosedIssues+1, repoAfter.NumClosedIssues)
+
+ // API response
+ assert.Equal(t, api.StateClosed, apiIssue.State)
+ assert.Equal(t, milestone, apiIssue.Milestone.ID)
+ assert.Equal(t, body, apiIssue.Body)
+ assert.True(t, apiIssue.Deadline == nil)
+ assert.Equal(t, title, apiIssue.Title)
+
+ // in database
+ assert.Equal(t, api.StateClosed, issueAfter.State())
+ assert.Equal(t, milestone, issueAfter.MilestoneID)
+ assert.Equal(t, int64(0), int64(issueAfter.DeadlineUnix))
+ assert.Equal(t, body, issueAfter.Content)
+ assert.Equal(t, title, issueAfter.Title)
+}
+
+func TestAPISearchIssues(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ token := getUserToken(t, "user2")
+
+ // as this API was used in the frontend, it uses UI page size
+ expectedIssueCount := 15 // from the fixtures
+ if expectedIssueCount > setting.UI.IssuePagingNum {
+ expectedIssueCount = setting.UI.IssuePagingNum
+ }
+
+ link, _ := url.Parse("/api/v1/repos/issues/search")
+ query := url.Values{"token": {getUserToken(t, "user1")}}
+ var apiIssues []*api.Issue
+
+ link.RawQuery = query.Encode()
+ req := NewRequest(t, "GET", link.String())
+ resp := MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, expectedIssueCount)
+
+ since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801
+ before := time.Unix(999307200, 0).Format(time.RFC3339)
+ query.Add("since", since)
+ query.Add("before", before)
+ query.Add("token", token)
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 8)
+ query.Del("since")
+ query.Del("before")
+
+ query.Add("state", "closed")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ query.Set("state", "all")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count"))
+ assert.Len(t, apiIssues, 17)
+
+ query.Add("limit", "10")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count"))
+ assert.Len(t, apiIssues, 10)
+
+ query = url.Values{"assigned": {"true"}, "state": {"all"}, "token": {token}}
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ query = url.Values{"milestones": {"milestone1"}, "state": {"all"}, "token": {token}}
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 1)
+
+ query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}, "token": {token}}
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ query = url.Values{"owner": {"user2"}, "token": {token}} // user
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 6)
+
+ query = url.Values{"owner": {"user3"}, "token": {token}} // organization
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 5)
+
+ query = url.Values{"owner": {"user3"}, "team": {"team1"}, "token": {token}} // organization + team
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+}
+
+func TestAPISearchIssuesWithLabels(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // as this API was used in the frontend, it uses UI page size
+ expectedIssueCount := 15 // from the fixtures
+ if expectedIssueCount > setting.UI.IssuePagingNum {
+ expectedIssueCount = setting.UI.IssuePagingNum
+ }
+
+ link, _ := url.Parse("/api/v1/repos/issues/search")
+ query := url.Values{"token": {getUserToken(t, "user1")}}
+ var apiIssues []*api.Issue
+
+ link.RawQuery = query.Encode()
+ req := NewRequest(t, "GET", link.String())
+ resp := MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, expectedIssueCount)
+
+ query.Add("labels", "label1")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // multiple labels
+ query.Set("labels", "label1,label2")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // an org label
+ query.Set("labels", "orglabel4")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 1)
+
+ // org and repo label
+ query.Set("labels", "label2,orglabel4")
+ query.Add("state", "all")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // org and repo label which share the same issue
+ query.Set("labels", "label1,orglabel4")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+}
diff --git a/tests/integration/api_issue_tracked_time_test.go b/tests/integration/api_issue_tracked_time_test.go
new file mode 100644
index 0000000000..6e2c77030c
--- /dev/null
+++ b/tests/integration/api_issue_tracked_time_test.go
@@ -0,0 +1,125 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIGetTrackedTimes(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ assert.NoError(t, issue2.LoadRepo(db.DefaultContext))
+
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/times?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiTimes api.TrackedTimeList
+ DecodeJSON(t, resp, &apiTimes)
+ expect, err := issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: issue2.ID})
+ assert.NoError(t, err)
+ assert.Len(t, apiTimes, 3)
+
+ for i, time := range expect {
+ assert.Equal(t, time.ID, apiTimes[i].ID)
+ assert.EqualValues(t, issue2.Title, apiTimes[i].Issue.Title)
+ assert.EqualValues(t, issue2.ID, apiTimes[i].IssueID)
+ assert.Equal(t, time.Created.Unix(), apiTimes[i].Created.Unix())
+ assert.Equal(t, time.Time, apiTimes[i].Time)
+ user, err := user_model.GetUserByID(time.UserID)
+ assert.NoError(t, err)
+ assert.Equal(t, user.Name, apiTimes[i].UserName)
+ }
+
+ // test filter
+ since := "2000-01-01T00%3A00%3A02%2B00%3A00" // 946684802
+ before := "2000-01-01T00%3A00%3A12%2B00%3A00" // 946684812
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/times?since=%s&before=%s&token=%s", user2.Name, issue2.Repo.Name, issue2.Index, since, before, token)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var filterAPITimes api.TrackedTimeList
+ DecodeJSON(t, resp, &filterAPITimes)
+ assert.Len(t, filterAPITimes, 2)
+ assert.Equal(t, int64(3), filterAPITimes[0].ID)
+ assert.Equal(t, int64(6), filterAPITimes[1].ID)
+}
+
+func TestAPIDeleteTrackedTime(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ time6 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 6})
+ issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ assert.NoError(t, issue2.LoadRepo(db.DefaultContext))
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Deletion not allowed
+ req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, time6.ID, token)
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3})
+ req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID, token)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ // Delete non existing time
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Reset time of user 2 on issue 2
+ trackedSeconds, err := issues_model.GetTrackedSeconds(db.DefaultContext, issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
+ assert.NoError(t, err)
+ assert.Equal(t, int64(3661), trackedSeconds)
+
+ req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, token)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ trackedSeconds, err = issues_model.GetTrackedSeconds(db.DefaultContext, issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
+ assert.NoError(t, err)
+ assert.Equal(t, int64(0), trackedSeconds)
+}
+
+func TestAPIAddTrackedTimes(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ assert.NoError(t, issue2.LoadRepo(db.DefaultContext))
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ session := loginUser(t, admin.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/times?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, token)
+
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.AddTimeOption{
+ Time: 33,
+ User: user2.Name,
+ Created: time.Unix(947688818, 0),
+ })
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiNewTime api.TrackedTime
+ DecodeJSON(t, resp, &apiNewTime)
+
+ assert.EqualValues(t, 33, apiNewTime.Time)
+ assert.EqualValues(t, user2.ID, apiNewTime.UserID)
+ assert.EqualValues(t, 947688818, apiNewTime.Created.Unix())
+}
diff --git a/tests/integration/api_keys_test.go b/tests/integration/api_keys_test.go
new file mode 100644
index 0000000000..1cb0b20ffe
--- /dev/null
+++ b/tests/integration/api_keys_test.go
@@ -0,0 +1,201 @@
+// Copyright 2017 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViewDeployKeysNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/keys")
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
+
+func TestCreateDeployKeyNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/keys", api.CreateKeyOption{
+ Title: "title",
+ Key: "key",
+ })
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
+
+func TestGetDeployKeyNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/keys/1")
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
+
+func TestDeleteDeployKeyNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo1/keys/1")
+ MakeRequest(t, req, http.StatusUnauthorized)
+}
+
+func TestCreateReadOnlyDeployKey(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo1"})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", repoOwner.Name, repo.Name, token)
+ rawKeyBody := api.CreateKeyOption{
+ Title: "read-only",
+ Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+ ReadOnly: true,
+ }
+ req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var newDeployKey api.DeployKey
+ DecodeJSON(t, resp, &newDeployKey)
+ unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
+ ID: newDeployKey.ID,
+ Name: rawKeyBody.Title,
+ Content: rawKeyBody.Key,
+ Mode: perm.AccessModeRead,
+ })
+}
+
+func TestCreateReadWriteDeployKey(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo1"})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", repoOwner.Name, repo.Name, token)
+ rawKeyBody := api.CreateKeyOption{
+ Title: "read-write",
+ Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+ }
+ req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var newDeployKey api.DeployKey
+ DecodeJSON(t, resp, &newDeployKey)
+ unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
+ ID: newDeployKey.ID,
+ Name: rawKeyBody.Title,
+ Content: rawKeyBody.Key,
+ Mode: perm.AccessModeWrite,
+ })
+}
+
+func TestCreateUserKey(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+ session := loginUser(t, "user1")
+ token := url.QueryEscape(getTokenForLoggedInUser(t, session))
+ keysURL := fmt.Sprintf("/api/v1/user/keys?token=%s", token)
+ keyType := "ssh-rsa"
+ keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM="
+ rawKeyBody := api.CreateKeyOption{
+ Title: "test-key",
+ Key: keyType + " " + keyContent,
+ }
+ req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var newPublicKey api.PublicKey
+ DecodeJSON(t, resp, &newPublicKey)
+ fingerprint, err := asymkey_model.CalcFingerprint(rawKeyBody.Key)
+ assert.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, &asymkey_model.PublicKey{
+ ID: newPublicKey.ID,
+ OwnerID: user.ID,
+ Name: rawKeyBody.Title,
+ Fingerprint: fingerprint,
+ Mode: perm.AccessModeWrite,
+ })
+
+ // Search by fingerprint
+ fingerprintURL := fmt.Sprintf("/api/v1/user/keys?token=%s&fingerprint=%s", token, newPublicKey.Fingerprint)
+
+ req = NewRequest(t, "GET", fingerprintURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ var fingerprintPublicKeys []api.PublicKey
+ DecodeJSON(t, resp, &fingerprintPublicKeys)
+ assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
+ assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
+ assert.Equal(t, user.ID, fingerprintPublicKeys[0].Owner.ID)
+
+ fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", user.Name, token, newPublicKey.Fingerprint)
+
+ req = NewRequest(t, "GET", fingerprintURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &fingerprintPublicKeys)
+ assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
+ assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
+ assert.Equal(t, user.ID, fingerprintPublicKeys[0].Owner.ID)
+
+ // Fail search by fingerprint
+ fingerprintURL = fmt.Sprintf("/api/v1/user/keys?token=%s&fingerprint=%sA", token, newPublicKey.Fingerprint)
+
+ req = NewRequest(t, "GET", fingerprintURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &fingerprintPublicKeys)
+ assert.Len(t, fingerprintPublicKeys, 0)
+
+ // Fail searching for wrong users key
+ fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", "user2", token, newPublicKey.Fingerprint)
+ req = NewRequest(t, "GET", fingerprintURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &fingerprintPublicKeys)
+ assert.Len(t, fingerprintPublicKeys, 0)
+
+ // Now login as user 2
+ session2 := loginUser(t, "user2")
+ token2 := url.QueryEscape(getTokenForLoggedInUser(t, session2))
+
+ // Should find key even though not ours, but we shouldn't know whose it is
+ fingerprintURL = fmt.Sprintf("/api/v1/user/keys?token=%s&fingerprint=%s", token2, newPublicKey.Fingerprint)
+ req = NewRequest(t, "GET", fingerprintURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &fingerprintPublicKeys)
+ assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
+ assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
+ assert.Nil(t, fingerprintPublicKeys[0].Owner)
+
+ // Should find key even though not ours, but we shouldn't know whose it is
+ fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", user.Name, token2, newPublicKey.Fingerprint)
+
+ req = NewRequest(t, "GET", fingerprintURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &fingerprintPublicKeys)
+ assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
+ assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
+ assert.Nil(t, fingerprintPublicKeys[0].Owner)
+
+ // Fail when searching for key if it is not ours
+ fingerprintURL = fmt.Sprintf("/api/v1/users/%s/keys?token=%s&fingerprint=%s", "user2", token2, newPublicKey.Fingerprint)
+ req = NewRequest(t, "GET", fingerprintURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &fingerprintPublicKeys)
+ assert.Len(t, fingerprintPublicKeys, 0)
+}
diff --git a/tests/integration/api_nodeinfo_test.go b/tests/integration/api_nodeinfo_test.go
new file mode 100644
index 0000000000..76f9105a51
--- /dev/null
+++ b/tests/integration/api_nodeinfo_test.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/routers"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNodeinfo(t *testing.T) {
+ setting.Federation.Enabled = true
+ c = routers.NormalRoutes(context.TODO())
+ defer func() {
+ setting.Federation.Enabled = false
+ c = routers.NormalRoutes(context.TODO())
+ }()
+
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ req := NewRequestf(t, "GET", "/api/v1/nodeinfo")
+ resp := MakeRequest(t, req, http.StatusOK)
+ var nodeinfo api.NodeInfo
+ DecodeJSON(t, resp, &nodeinfo)
+ assert.True(t, nodeinfo.OpenRegistrations)
+ assert.Equal(t, "gitea", nodeinfo.Software.Name)
+ assert.Equal(t, 23, nodeinfo.Usage.Users.Total)
+ assert.Equal(t, 17, nodeinfo.Usage.LocalPosts)
+ assert.Equal(t, 2, nodeinfo.Usage.LocalComments)
+ })
+}
diff --git a/tests/integration/api_notification_test.go b/tests/integration/api_notification_test.go
new file mode 100644
index 0000000000..bf85520bb5
--- /dev/null
+++ b/tests/integration/api_notification_test.go
@@ -0,0 +1,193 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPINotification(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
+ assert.NoError(t, thread5.LoadAttributes())
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // -- GET /notifications --
+ // test filter
+ since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?since=%s&token=%s", since, token))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiNL []api.NotificationThread
+ DecodeJSON(t, resp, &apiNL)
+
+ assert.Len(t, apiNL, 1)
+ assert.EqualValues(t, 5, apiNL[0].ID)
+
+ // test filter
+ before := "2000-01-01T01%3A06%3A59%2B00%3A00" // 946688819
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?all=%s&before=%s&token=%s", "true", before, token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiNL)
+
+ assert.Len(t, apiNL, 3)
+ assert.EqualValues(t, 4, apiNL[0].ID)
+ assert.True(t, apiNL[0].Unread)
+ assert.False(t, apiNL[0].Pinned)
+ assert.EqualValues(t, 3, apiNL[1].ID)
+ assert.False(t, apiNL[1].Unread)
+ assert.True(t, apiNL[1].Pinned)
+ assert.EqualValues(t, 2, apiNL[2].ID)
+ assert.False(t, apiNL[2].Unread)
+ assert.False(t, apiNL[2].Pinned)
+
+ // -- GET /repos/{owner}/{repo}/notifications --
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?status-types=unread&token=%s", user2.Name, repo1.Name, token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiNL)
+
+ assert.Len(t, apiNL, 1)
+ assert.EqualValues(t, 4, apiNL[0].ID)
+
+ // -- GET /repos/{owner}/{repo}/notifications -- multiple status-types
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?status-types=unread&status-types=pinned&token=%s", user2.Name, repo1.Name, token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiNL)
+
+ assert.Len(t, apiNL, 2)
+ assert.EqualValues(t, 4, apiNL[0].ID)
+ assert.True(t, apiNL[0].Unread)
+ assert.False(t, apiNL[0].Pinned)
+ assert.EqualValues(t, 3, apiNL[1].ID)
+ assert.False(t, apiNL[1].Unread)
+ assert.True(t, apiNL[1].Pinned)
+
+ // -- GET /notifications/threads/{id} --
+ // get forbidden
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", 1, token))
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // get own
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiN api.NotificationThread
+ DecodeJSON(t, resp, &apiN)
+
+ assert.EqualValues(t, 5, apiN.ID)
+ assert.False(t, apiN.Pinned)
+ assert.True(t, apiN.Unread)
+ assert.EqualValues(t, "issue4", apiN.Subject.Title)
+ assert.EqualValues(t, "Issue", apiN.Subject.Type)
+ assert.EqualValues(t, thread5.Issue.APIURL(), apiN.Subject.URL)
+ assert.EqualValues(t, thread5.Repository.HTMLURL(), apiN.Repository.HTMLURL)
+
+ new := struct {
+ New int64 `json:"new"`
+ }{}
+
+ // -- check notifications --
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/new?token=%s", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &new)
+ assert.True(t, new.New > 0)
+
+ // -- mark notifications as read --
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiNL)
+ assert.Len(t, apiNL, 2)
+
+ lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 <- only Notification 4 is in this filter ...
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token))
+ session.MakeRequest(t, req, http.StatusResetContent)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiNL)
+ assert.Len(t, apiNL, 1)
+
+ // -- PATCH /notifications/threads/{id} --
+ req = NewRequest(t, "PATCH", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token))
+ session.MakeRequest(t, req, http.StatusResetContent)
+
+ assert.Equal(t, activities_model.NotificationStatusUnread, thread5.Status)
+ thread5 = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
+ assert.Equal(t, activities_model.NotificationStatusRead, thread5.Status)
+
+ // -- check notifications --
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/new?token=%s", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &new)
+ assert.True(t, new.New == 0)
+}
+
+func TestAPINotificationPUT(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
+ assert.NoError(t, thread5.LoadAttributes())
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Check notifications are as expected
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?all=true&token=%s", token))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiNL []api.NotificationThread
+ DecodeJSON(t, resp, &apiNL)
+
+ assert.Len(t, apiNL, 4)
+ assert.EqualValues(t, 5, apiNL[0].ID)
+ assert.True(t, apiNL[0].Unread)
+ assert.False(t, apiNL[0].Pinned)
+ assert.EqualValues(t, 4, apiNL[1].ID)
+ assert.True(t, apiNL[1].Unread)
+ assert.False(t, apiNL[1].Pinned)
+ assert.EqualValues(t, 3, apiNL[2].ID)
+ assert.False(t, apiNL[2].Unread)
+ assert.True(t, apiNL[2].Pinned)
+ assert.EqualValues(t, 2, apiNL[3].ID)
+ assert.False(t, apiNL[3].Unread)
+ assert.False(t, apiNL[3].Pinned)
+
+ //
+ // Notification ID 2 is the only one with status-type read & pinned
+ // change it to unread.
+ //
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/notifications?status-types=read&status-type=pinned&to-status=unread&token=%s", token))
+ resp = session.MakeRequest(t, req, http.StatusResetContent)
+ DecodeJSON(t, resp, &apiNL)
+ assert.Len(t, apiNL, 1)
+ assert.EqualValues(t, 2, apiNL[0].ID)
+ assert.True(t, apiNL[0].Unread)
+ assert.False(t, apiNL[0].Pinned)
+
+ //
+ // Now nofication ID 2 is the first in the list and is unread.
+ //
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?all=true&token=%s", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiNL)
+
+ assert.Len(t, apiNL, 4)
+ assert.EqualValues(t, 2, apiNL[0].ID)
+ assert.True(t, apiNL[0].Unread)
+ assert.False(t, apiNL[0].Pinned)
+}
diff --git a/tests/integration/api_oauth2_apps_test.go b/tests/integration/api_oauth2_apps_test.go
new file mode 100644
index 0000000000..fe3525724e
--- /dev/null
+++ b/tests/integration/api_oauth2_apps_test.go
@@ -0,0 +1,166 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.package models
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestOAuth2Application(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testAPICreateOAuth2Application(t)
+ testAPIListOAuth2Applications(t)
+ testAPIGetOAuth2Application(t)
+ testAPIUpdateOAuth2Application(t)
+ testAPIDeleteOAuth2Application(t)
+}
+
+func testAPICreateOAuth2Application(t *testing.T) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ appBody := api.CreateOAuth2ApplicationOptions{
+ Name: "test-app-1",
+ RedirectURIs: []string{
+ "http://www.google.com",
+ },
+ }
+
+ req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody)
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ var createdApp *api.OAuth2Application
+ DecodeJSON(t, resp, &createdApp)
+
+ assert.EqualValues(t, appBody.Name, createdApp.Name)
+ assert.Len(t, createdApp.ClientSecret, 56)
+ assert.Len(t, createdApp.ClientID, 36)
+ assert.NotEmpty(t, createdApp.Created)
+ assert.EqualValues(t, appBody.RedirectURIs[0], createdApp.RedirectURIs[0])
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{UID: user.ID, Name: createdApp.Name})
+}
+
+func testAPIListOAuth2Applications(t *testing.T) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ existApp := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{
+ UID: user.ID,
+ Name: "test-app-1",
+ RedirectURIs: []string{
+ "http://www.google.com",
+ },
+ })
+
+ urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2?token=%s", token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var appList api.OAuth2ApplicationList
+ DecodeJSON(t, resp, &appList)
+ expectedApp := appList[0]
+
+ assert.EqualValues(t, existApp.Name, expectedApp.Name)
+ assert.EqualValues(t, existApp.ClientID, expectedApp.ClientID)
+ assert.Len(t, expectedApp.ClientID, 36)
+ assert.Empty(t, expectedApp.ClientSecret)
+ assert.EqualValues(t, existApp.RedirectURIs[0], expectedApp.RedirectURIs[0])
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
+}
+
+func testAPIDeleteOAuth2Application(t *testing.T) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ oldApp := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{
+ UID: user.ID,
+ Name: "test-app-1",
+ })
+
+ urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d?token=%s", oldApp.ID, token)
+ req := NewRequest(t, "DELETE", urlStr)
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ unittest.AssertNotExistsBean(t, &auth.OAuth2Application{UID: oldApp.UID, Name: oldApp.Name})
+
+ // Delete again will return not found
+ req = NewRequest(t, "DELETE", urlStr)
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
+
+func testAPIGetOAuth2Application(t *testing.T) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ existApp := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{
+ UID: user.ID,
+ Name: "test-app-1",
+ RedirectURIs: []string{
+ "http://www.google.com",
+ },
+ })
+
+ urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d?token=%s", existApp.ID, token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var app api.OAuth2Application
+ DecodeJSON(t, resp, &app)
+ expectedApp := app
+
+ assert.EqualValues(t, existApp.Name, expectedApp.Name)
+ assert.EqualValues(t, existApp.ClientID, expectedApp.ClientID)
+ assert.Len(t, expectedApp.ClientID, 36)
+ assert.Empty(t, expectedApp.ClientSecret)
+ assert.Len(t, expectedApp.RedirectURIs, 1)
+ assert.EqualValues(t, existApp.RedirectURIs[0], expectedApp.RedirectURIs[0])
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
+}
+
+func testAPIUpdateOAuth2Application(t *testing.T) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ existApp := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{
+ UID: user.ID,
+ Name: "test-app-1",
+ RedirectURIs: []string{
+ "http://www.google.com",
+ },
+ })
+
+ appBody := api.CreateOAuth2ApplicationOptions{
+ Name: "test-app-1",
+ RedirectURIs: []string{
+ "http://www.google.com/",
+ "http://www.github.com/",
+ },
+ }
+
+ urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d", existApp.ID)
+ req := NewRequestWithJSON(t, "PATCH", urlStr, &appBody)
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var app api.OAuth2Application
+ DecodeJSON(t, resp, &app)
+ expectedApp := app
+
+ assert.Len(t, expectedApp.RedirectURIs, 2)
+ assert.EqualValues(t, expectedApp.RedirectURIs[0], appBody.RedirectURIs[0])
+ assert.EqualValues(t, expectedApp.RedirectURIs[1], appBody.RedirectURIs[1])
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
+}
diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go
new file mode 100644
index 0000000000..70bb17bee2
--- /dev/null
+++ b/tests/integration/api_org_test.go
@@ -0,0 +1,153 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIOrgCreate(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ token := getUserToken(t, "user1")
+
+ org := api.CreateOrgOption{
+ UserName: "user1_org",
+ FullName: "User1's organization",
+ Description: "This organization created by user1",
+ Website: "https://try.gitea.io",
+ Location: "Shanghai",
+ Visibility: "limited",
+ }
+ req := NewRequestWithJSON(t, "POST", "/api/v1/orgs?token="+token, &org)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ var apiOrg api.Organization
+ DecodeJSON(t, resp, &apiOrg)
+
+ assert.Equal(t, org.UserName, apiOrg.UserName)
+ assert.Equal(t, org.FullName, apiOrg.FullName)
+ assert.Equal(t, org.Description, apiOrg.Description)
+ assert.Equal(t, org.Website, apiOrg.Website)
+ assert.Equal(t, org.Location, apiOrg.Location)
+ assert.Equal(t, org.Visibility, apiOrg.Visibility)
+
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: org.UserName,
+ LowerName: strings.ToLower(org.UserName),
+ FullName: org.FullName,
+ })
+
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s?token=%s", org.UserName, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiOrg)
+ assert.EqualValues(t, org.UserName, apiOrg.UserName)
+
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos?token=%s", org.UserName, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ var repos []*api.Repository
+ DecodeJSON(t, resp, &repos)
+ for _, repo := range repos {
+ assert.False(t, repo.Private)
+ }
+
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members?token=%s", org.UserName, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ // user1 on this org is public
+ var users []*api.User
+ DecodeJSON(t, resp, &users)
+ assert.Len(t, users, 1)
+ assert.EqualValues(t, "user1", users[0].UserName)
+ })
+}
+
+func TestAPIOrgEdit(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ session := loginUser(t, "user1")
+
+ token := getTokenForLoggedInUser(t, session)
+ org := api.EditOrgOption{
+ FullName: "User3 organization new full name",
+ Description: "A new description",
+ Website: "https://try.gitea.io/new",
+ Location: "Beijing",
+ Visibility: "private",
+ }
+ req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/user3?token="+token, &org)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiOrg api.Organization
+ DecodeJSON(t, resp, &apiOrg)
+
+ assert.Equal(t, "user3", apiOrg.UserName)
+ assert.Equal(t, org.FullName, apiOrg.FullName)
+ assert.Equal(t, org.Description, apiOrg.Description)
+ assert.Equal(t, org.Website, apiOrg.Website)
+ assert.Equal(t, org.Location, apiOrg.Location)
+ assert.Equal(t, org.Visibility, apiOrg.Visibility)
+ })
+}
+
+func TestAPIOrgEditBadVisibility(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ session := loginUser(t, "user1")
+
+ token := getTokenForLoggedInUser(t, session)
+ org := api.EditOrgOption{
+ FullName: "User3 organization new full name",
+ Description: "A new description",
+ Website: "https://try.gitea.io/new",
+ Location: "Beijing",
+ Visibility: "badvisibility",
+ }
+ req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/user3?token="+token, &org)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+}
+
+func TestAPIOrgDeny(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ setting.Service.RequireSignInView = true
+ defer func() {
+ setting.Service.RequireSignInView = false
+ }()
+
+ orgName := "user1_org"
+ req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+}
+
+func TestAPIGetAll(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequestf(t, "GET", "/api/v1/orgs")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var apiOrgList []*api.Organization
+ DecodeJSON(t, resp, &apiOrgList)
+
+ assert.Len(t, apiOrgList, 7)
+ assert.Equal(t, "org25", apiOrgList[0].FullName)
+ assert.Equal(t, "public", apiOrgList[0].Visibility)
+}
diff --git a/tests/integration/api_packages_composer_test.go b/tests/integration/api_packages_composer_test.go
new file mode 100644
index 0000000000..90285f78d3
--- /dev/null
+++ b/tests/integration/api_packages_composer_test.go
@@ -0,0 +1,215 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+ "net/http"
+ neturl "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ composer_module "code.gitea.io/gitea/modules/packages/composer"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers/api/packages/composer"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageComposer(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ vendorName := "gitea"
+ projectName := "composer-package"
+ packageName := vendorName + "/" + projectName
+ packageVersion := "1.0.3"
+ packageDescription := "Package Description"
+ packageType := "composer-plugin"
+ packageAuthor := "Gitea Authors"
+ packageLicense := "MIT"
+
+ var buf bytes.Buffer
+ archive := zip.NewWriter(&buf)
+ w, _ := archive.Create("composer.json")
+ w.Write([]byte(`{
+ "name": "` + packageName + `",
+ "description": "` + packageDescription + `",
+ "type": "` + packageType + `",
+ "license": "` + packageLicense + `",
+ "authors": [
+ {
+ "name": "` + packageAuthor + `"
+ }
+ ]
+ }`))
+ archive.Close()
+ content := buf.Bytes()
+
+ url := fmt.Sprintf("%sapi/packages/%s/composer", setting.AppURL, user.Name)
+
+ t.Run("ServiceIndex", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/packages.json", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result composer.ServiceIndexResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, url+"/search.json?q=%query%&type=%type%", result.SearchTemplate)
+ assert.Equal(t, url+"/p2/%package%.json", result.MetadataTemplate)
+ assert.Equal(t, url+"/list.json", result.PackageList)
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ t.Run("MissingVersion", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("Valid", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadURL := url + "?version=" + packageVersion
+
+ req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeComposer)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &composer_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, fmt.Sprintf("%s-%s.%s.zip", vendorName, projectName, packageVersion), pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeComposer)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(0), pvs[0].DownloadCount)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/files/%s/%s/%s", url, neturl.PathEscape(packageName), neturl.PathEscape(pvs[0].LowerVersion), neturl.PathEscape(pfs[0].LowerName)))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+
+ pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeComposer)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("SearchService", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Type string
+ Page int
+ PerPage int
+ ExpectedTotal int64
+ ExpectedResults int
+ }{
+ {"", "", 0, 0, 1, 1},
+ {"", "", 1, 1, 1, 1},
+ {"test", "", 1, 0, 0, 0},
+ {"gitea", "", 1, 1, 1, 1},
+ {"gitea", "", 2, 1, 1, 0},
+ {"", packageType, 1, 1, 1, 1},
+ {"gitea", packageType, 1, 1, 1, 1},
+ {"gitea", "dummy", 1, 1, 0, 0},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/search.json?q=%s&type=%s&page=%d&per_page=%d", url, c.Query, c.Type, c.Page, c.PerPage))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result composer.SearchResultResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, c.ExpectedTotal, result.Total, "case %d: unexpected total hits", i)
+ assert.Len(t, result.Results, c.ExpectedResults, "case %d: unexpected result count", i)
+ }
+ })
+
+ t.Run("EnumeratePackages", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", url+"/list.json")
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string][]string
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, "packageNames")
+ names := result["packageNames"]
+ assert.Len(t, names, 1)
+ assert.Equal(t, packageName, names[0])
+ })
+
+ t.Run("PackageMetadata", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result composer.PackageMetadataResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result.Packages, packageName)
+ pkgs := result.Packages[packageName]
+ assert.Len(t, pkgs, 1)
+ assert.Equal(t, packageName, pkgs[0].Name)
+ assert.Equal(t, packageVersion, pkgs[0].Version)
+ assert.Equal(t, packageType, pkgs[0].Type)
+ assert.Equal(t, packageDescription, pkgs[0].Description)
+ assert.Len(t, pkgs[0].Authors, 1)
+ assert.Equal(t, packageAuthor, pkgs[0].Authors[0].Name)
+ assert.Equal(t, "zip", pkgs[0].Dist.Type)
+ assert.Equal(t, "7b40bfd6da811b2b78deec1e944f156dbb2c747b", pkgs[0].Dist.Checksum)
+ })
+}
diff --git a/tests/integration/api_packages_conan_test.go b/tests/integration/api_packages_conan_test.go
new file mode 100644
index 0000000000..5b34417343
--- /dev/null
+++ b/tests/integration/api_packages_conan_test.go
@@ -0,0 +1,725 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ stdurl "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ conan_model "code.gitea.io/gitea/models/packages/conan"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ conan_module "code.gitea.io/gitea/modules/packages/conan"
+ "code.gitea.io/gitea/modules/setting"
+ conan_router "code.gitea.io/gitea/routers/api/packages/conan"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ conanfileName = "conanfile.py"
+ conaninfoName = "conaninfo.txt"
+
+ conanLicense = "MIT"
+ conanAuthor = "Gitea <info@gitea.io>"
+ conanHomepage = "https://gitea.io/"
+ conanURL = "https://gitea.com/"
+ conanDescription = "Description of ConanPackage"
+ conanTopic = "gitea"
+
+ conanPackageReference = "dummyreference"
+
+ contentConaninfo = `[settings]
+ arch=x84_64
+
+[requires]
+ fmt/7.1.3
+
+[options]
+ shared=False
+
+[full_settings]
+ arch=x84_64
+
+[full_requires]
+ fmt/7.1.3
+
+[full_options]
+ shared=False
+
+[recipe_hash]
+ 74714915a51073acb548ca1ce29afbac
+
+[env]
+CC=gcc-10`
+)
+
+func addTokenAuthHeader(request *http.Request, token string) *http.Request {
+ request.Header.Set("Authorization", token)
+ return request
+}
+
+func buildConanfileContent(name, version string) string {
+ return `from conans import ConanFile, CMake, tools
+
+class ConanPackageConan(ConanFile):
+ name = "` + name + `"
+ version = "` + version + `"
+ license = "` + conanLicense + `"
+ author = "` + conanAuthor + `"
+ homepage = "` + conanHomepage + `"
+ url = "` + conanURL + `"
+ description = "` + conanDescription + `"
+ topics = ("` + conanTopic + `")
+ settings = "os", "compiler", "build_type", "arch"
+ options = {"shared": [True, False], "fPIC": [True, False]}
+ default_options = {"shared": False, "fPIC": True}
+ generators = "cmake"`
+}
+
+func uploadConanPackageV1(t *testing.T, baseURL, token, name, version, user, channel string) {
+ contentConanfile := buildConanfileContent(name, version)
+
+ recipeURL := fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", baseURL, name, version, user, channel)
+
+ req := NewRequest(t, "GET", recipeURL)
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", recipeURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", recipeURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/upload_urls", recipeURL))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/upload_urls", recipeURL), map[string]int64{
+ conanfileName: int64(len(contentConanfile)),
+ "removed.txt": 0,
+ })
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ uploadURLs := make(map[string]string)
+ DecodeJSON(t, resp, &uploadURLs)
+
+ assert.Contains(t, uploadURLs, conanfileName)
+ assert.NotContains(t, uploadURLs, "removed.txt")
+
+ uploadURL := uploadURLs[conanfileName]
+ assert.NotEmpty(t, uploadURL)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, strings.NewReader(contentConanfile))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ packageURL := fmt.Sprintf("%s/packages/%s", recipeURL, conanPackageReference)
+
+ req = NewRequest(t, "GET", packageURL)
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", packageURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", packageURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/upload_urls", packageURL))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/upload_urls", packageURL), map[string]int64{
+ conaninfoName: int64(len(contentConaninfo)),
+ "removed.txt": 0,
+ })
+ req = addTokenAuthHeader(req, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ uploadURLs = make(map[string]string)
+ DecodeJSON(t, resp, &uploadURLs)
+
+ assert.Contains(t, uploadURLs, conaninfoName)
+ assert.NotContains(t, uploadURLs, "removed.txt")
+
+ uploadURL = uploadURLs[conaninfoName]
+ assert.NotEmpty(t, uploadURL)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, strings.NewReader(contentConaninfo))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+}
+
+func uploadConanPackageV2(t *testing.T, baseURL, token, name, version, user, channel, recipeRevision, packageRevision string) {
+ contentConanfile := buildConanfileContent(name, version)
+
+ recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", baseURL, name, version, user, channel, recipeRevision)
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/files/%s", recipeURL, conanfileName), strings.NewReader(contentConanfile))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/files", recipeURL))
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var list *struct {
+ Files map[string]interface{} `json:"files"`
+ }
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Files, 1)
+ assert.Contains(t, list.Files, conanfileName)
+
+ packageURL := fmt.Sprintf("%s/packages/%s/revisions/%s", recipeURL, conanPackageReference, packageRevision)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/files", packageURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/files/%s", packageURL, conaninfoName), strings.NewReader(contentConaninfo))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/files", packageURL))
+ req = addTokenAuthHeader(req, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ list = nil
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Files, 1)
+ assert.Contains(t, list.Files, conaninfoName)
+}
+
+func TestPackageConan(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ name := "ConanPackage"
+ version1 := "1.2"
+ version2 := "1.3"
+ user1 := "dummy"
+ user2 := "gitea"
+ channel1 := "test"
+ channel2 := "final"
+ revision1 := "rev1"
+ revision2 := "rev2"
+
+ url := fmt.Sprintf("%sapi/packages/%s/conan", setting.AppURL, user.Name)
+
+ t.Run("v1", func(t *testing.T) {
+ t.Run("Ping", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/ping", url))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "revisions", resp.Header().Get("X-Conan-Server-Capabilities"))
+ })
+
+ token := ""
+
+ t.Run("Authenticate", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ body := resp.Body.String()
+ assert.NotEmpty(t, body)
+
+ token = fmt.Sprintf("Bearer %s", body)
+ })
+
+ t.Run("CheckCredentials", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/check_credentials", url))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadConanPackageV1(t, url, token, name, version1, user1, channel1)
+
+ t.Run("Validate", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.Equal(t, name, pd.Package.Name)
+ assert.Equal(t, version1, pd.Version.Version)
+ assert.IsType(t, &conan_module.Metadata{}, pd.Metadata)
+ metadata := pd.Metadata.(*conan_module.Metadata)
+ assert.Equal(t, conanLicense, metadata.License)
+ assert.Equal(t, conanAuthor, metadata.Author)
+ assert.Equal(t, conanHomepage, metadata.ProjectURL)
+ assert.Equal(t, conanURL, metadata.RepositoryURL)
+ assert.Equal(t, conanDescription, metadata.Description)
+ assert.Equal(t, []string{conanTopic}, metadata.Keywords)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2)
+
+ for _, pf := range pfs {
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+
+ if pf.Name == conanfileName {
+ assert.True(t, pf.IsLead)
+
+ assert.Equal(t, int64(len(buildConanfileContent(name, version1))), pb.Size)
+ } else if pf.Name == conaninfoName {
+ assert.False(t, pf.IsLead)
+
+ assert.Equal(t, int64(len(contentConaninfo)), pb.Size)
+ } else {
+ assert.Fail(t, "unknown file: %s", pf.Name)
+ }
+ }
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ recipeURL := fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, name, version1, user1, channel1)
+
+ req := NewRequest(t, "GET", recipeURL)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ fileHashes := make(map[string]string)
+ DecodeJSON(t, resp, &fileHashes)
+ assert.Len(t, fileHashes, 1)
+ assert.Contains(t, fileHashes, conanfileName)
+ assert.Equal(t, "7abc52241c22090782c54731371847a8", fileHashes[conanfileName])
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", recipeURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ downloadURLs := make(map[string]string)
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conanfileName)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", recipeURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conanfileName)
+
+ req = NewRequest(t, "GET", downloadURLs[conanfileName])
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, buildConanfileContent(name, version1), resp.Body.String())
+
+ packageURL := fmt.Sprintf("%s/packages/%s", recipeURL, conanPackageReference)
+
+ req = NewRequest(t, "GET", packageURL)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ fileHashes = make(map[string]string)
+ DecodeJSON(t, resp, &fileHashes)
+ assert.Len(t, fileHashes, 1)
+ assert.Contains(t, fileHashes, conaninfoName)
+ assert.Equal(t, "7628bfcc5b17f1470c468621a78df394", fileHashes[conaninfoName])
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", packageURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ downloadURLs = make(map[string]string)
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conaninfoName)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", packageURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conaninfoName)
+
+ req = NewRequest(t, "GET", downloadURLs[conaninfoName])
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, contentConaninfo, resp.Body.String())
+ })
+
+ t.Run("Search", func(t *testing.T) {
+ uploadConanPackageV1(t, url, token, name, version2, user1, channel1)
+ uploadConanPackageV1(t, url, token, name, version1, user1, channel2)
+ uploadConanPackageV1(t, url, token, name, version1, user2, channel1)
+ uploadConanPackageV1(t, url, token, name, version1, user2, channel2)
+
+ t.Run("Recipe", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Expected []string
+ }{
+ {"ConanPackage", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.1", []string{}},
+ {"Conan*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@du*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final"}},
+ {"ConanPackage/1.2@du*/", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final"}},
+ {"ConanPackage/1.2@du*/*test", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/*st", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@gitea/*", []string{"ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"*/*@dummy", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final"}},
+ {"*/*@*/final", []string{"ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/final"}},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/conans/search?q=%s", url, stdurl.QueryEscape(c.Query)))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result *conan_router.SearchResult
+ DecodeJSON(t, resp, &result)
+
+ assert.ElementsMatch(t, c.Expected, result.Results, "case %d: unexpected result", i)
+ }
+ })
+
+ t.Run("Package", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s/search", url, name, version1, user1, channel2))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string]*conan_module.Conaninfo
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, conanPackageReference)
+ info := result[conanPackageReference]
+ assert.NotEmpty(t, info.Settings)
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Package", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ cases := []struct {
+ Channel string
+ References []string
+ }{
+ {channel1, []string{conanPackageReference}},
+ {channel2, []string{}},
+ }
+
+ for i, c := range cases {
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, c.Channel, conan_module.DefaultRevision)
+ references, err := conan_model.GetPackageReferences(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, references)
+
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s/packages/delete", url, name, version1, user1, c.Channel), map[string][]string{
+ "package_ids": c.References,
+ })
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ references, err = conan_model.GetPackageReferences(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Empty(t, references, "case %d: should be empty", i)
+ }
+ })
+
+ t.Run("Recipe", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ cases := []struct {
+ Channel string
+ }{
+ {channel1},
+ {channel2},
+ }
+
+ for i, c := range cases {
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, c.Channel, conan_module.DefaultRevision)
+ revisions, err := conan_model.GetRecipeRevisions(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, revisions)
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, name, version1, user1, c.Channel))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ revisions, err = conan_model.GetRecipeRevisions(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Empty(t, revisions, "case %d: should be empty", i)
+ }
+ })
+ })
+ })
+
+ t.Run("v2", func(t *testing.T) {
+ t.Run("Ping", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/ping", url))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "revisions", resp.Header().Get("X-Conan-Server-Capabilities"))
+ })
+
+ token := ""
+
+ t.Run("Authenticate", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ body := resp.Body.String()
+ assert.NotEmpty(t, body)
+
+ token = fmt.Sprintf("Bearer %s", body)
+ })
+
+ t.Run("CheckCredentials", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/check_credentials", url))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision1, revision1)
+
+ t.Run("Validate", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 2)
+ })
+ })
+
+ t.Run("Latest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s", url, name, version1, user1, channel1)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/latest", recipeURL))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ obj := make(map[string]string)
+ DecodeJSON(t, resp, &obj)
+ assert.Contains(t, obj, "revision")
+ assert.Equal(t, revision1, obj["revision"])
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/revisions/%s/packages/%s/latest", recipeURL, revision1, conanPackageReference))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ obj = make(map[string]string)
+ DecodeJSON(t, resp, &obj)
+ assert.Contains(t, obj, "revision")
+ assert.Equal(t, revision1, obj["revision"])
+ })
+
+ t.Run("ListRevisions", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision1, revision2)
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision2, revision1)
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision2, revision2)
+
+ recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions", url, name, version1, user1, channel1)
+
+ req := NewRequest(t, "GET", recipeURL)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type RevisionInfo struct {
+ Revision string `json:"revision"`
+ Time time.Time `json:"time"`
+ }
+
+ type RevisionList struct {
+ Revisions []*RevisionInfo `json:"revisions"`
+ }
+
+ var list *RevisionList
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Revisions, 2)
+ revs := make([]string, 0, len(list.Revisions))
+ for _, rev := range list.Revisions {
+ revs = append(revs, rev.Revision)
+ }
+ assert.ElementsMatch(t, []string{revision1, revision2}, revs)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/packages/%s/revisions", recipeURL, revision1, conanPackageReference))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Revisions, 2)
+ revs = make([]string, 0, len(list.Revisions))
+ for _, rev := range list.Revisions {
+ revs = append(revs, rev.Revision)
+ }
+ assert.ElementsMatch(t, []string{revision1, revision2}, revs)
+ })
+
+ t.Run("Search", func(t *testing.T) {
+ t.Run("Recipe", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Expected []string
+ }{
+ {"ConanPackage", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.1", []string{}},
+ {"Conan*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@du*", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/*test", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/*st", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@gitea/*", []string{"ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"*/*@dummy", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test"}},
+ {"*/*@*/final", []string{"ConanPackage/1.2@gitea/final"}},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/conans/search?q=%s", url, stdurl.QueryEscape(c.Query)))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result *conan_router.SearchResult
+ DecodeJSON(t, resp, &result)
+
+ assert.ElementsMatch(t, c.Expected, result.Results, "case %d: unexpected result", i)
+ }
+ })
+
+ t.Run("Package", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/search", url, name, version1, user1, channel1))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string]*conan_module.Conaninfo
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, conanPackageReference)
+ info := result[conanPackageReference]
+ assert.NotEmpty(t, info.Settings)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/search", url, name, version1, user1, channel1, revision1))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ result = make(map[string]*conan_module.Conaninfo)
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, conanPackageReference)
+ info = result[conanPackageReference]
+ assert.NotEmpty(t, info.Settings)
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Package", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, channel1, revision1)
+ pref, _ := conan_module.NewPackageReference(rref, conanPackageReference, conan_module.DefaultRevision)
+
+ checkPackageRevisionCount := func(count int) {
+ revisions, err := conan_model.GetPackageRevisions(db.DefaultContext, user.ID, pref)
+ assert.NoError(t, err)
+ assert.Len(t, revisions, count)
+ }
+ checkPackageReferenceCount := func(count int) {
+ references, err := conan_model.GetPackageReferences(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Len(t, references, count)
+ }
+
+ checkPackageRevisionCount(2)
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s/revisions/%s", url, name, version1, user1, channel1, revision1, conanPackageReference, revision1))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkPackageRevisionCount(1)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s", url, name, version1, user1, channel1, revision1, conanPackageReference))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkPackageRevisionCount(0)
+
+ rref = rref.WithRevision(revision2)
+
+ checkPackageReferenceCount(1)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages", url, name, version1, user1, channel1, revision2))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkPackageReferenceCount(0)
+ })
+
+ t.Run("Recipe", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, channel1, conan_module.DefaultRevision)
+
+ checkRecipeRevisionCount := func(count int) {
+ revisions, err := conan_model.GetRecipeRevisions(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Len(t, revisions, count)
+ }
+
+ checkRecipeRevisionCount(2)
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, name, version1, user1, channel1, revision1))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkRecipeRevisionCount(1)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s", url, name, version1, user1, channel1))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkRecipeRevisionCount(0)
+ })
+ })
+ })
+}
diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go
new file mode 100644
index 0000000000..adced5d661
--- /dev/null
+++ b/tests/integration/api_packages_container_test.go
@@ -0,0 +1,608 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ packages_model "code.gitea.io/gitea/models/packages"
+ container_model "code.gitea.io/gitea/models/packages/container"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ container_module "code.gitea.io/gitea/modules/packages/container"
+ "code.gitea.io/gitea/modules/packages/container/oci"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageContainer(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ has := func(l packages_model.PackagePropertyList, name string) bool {
+ for _, pp := range l {
+ if pp.Name == name {
+ return true
+ }
+ }
+ return false
+ }
+ getAllByName := func(l packages_model.PackagePropertyList, name string) []string {
+ values := make([]string, 0, len(l))
+ for _, pp := range l {
+ if pp.Name == name {
+ values = append(values, pp.Value)
+ }
+ }
+ return values
+ }
+
+ images := []string{"test", "te/st"}
+ tags := []string{"latest", "main"}
+ multiTag := "multi"
+
+ unknownDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+
+ blobDigest := "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
+ blobContent, _ := base64.StdEncoding.DecodeString(`H4sIAAAJbogA/2IYBaNgFIxYAAgAAP//Lq+17wAEAAA=`)
+
+ configDigest := "sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d"
+ configContent := `{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/true"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"container":"b89fe92a887d55c0961f02bdfbfd8ac3ddf66167db374770d2d9e9fab3311510","container_config":{"Hostname":"b89fe92a887d","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/true\"]"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"created":"2022-01-01T00:00:00.000000000Z","docker_version":"20.10.12","history":[{"created":"2022-01-01T00:00:00.000000000Z","created_by":"/bin/sh -c #(nop) COPY file:0e7589b0c800daaf6fa460d2677101e4676dd9491980210cb345480e513f3602 in /true "},{"created":"2022-01-01T00:00:00.000000001Z","created_by":"/bin/sh -c #(nop) CMD [\"/true\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0ff3b91bdf21ecdf2f2f3d4372c2098a14dbe06cd678e8f0a85fd4902d00e2e2"]}}`
+
+ manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6"
+ manifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeDockerManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
+
+ untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d"
+ untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
+
+ indexManifestDigest := "sha256:bab112d6efb9e7f221995caaaa880352feb5bd8b1faf52fae8d12c113aa123ec"
+ indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"` + oci.MediaTypeDockerManifest + `","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}`
+
+ anonymousToken := ""
+ userToken := ""
+
+ t.Run("Authenticate", func(t *testing.T) {
+ type TokenResponse struct {
+ Token string `json:"token"`
+ }
+
+ authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ resp := MakeRequest(t, req, http.StatusUnauthorized)
+
+ assert.ElementsMatch(t, authenticate, resp.Header().Values("WWW-Authenticate"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ tokenResponse := &TokenResponse{}
+ DecodeJSON(t, resp, &tokenResponse)
+
+ assert.NotEmpty(t, tokenResponse.Token)
+
+ anonymousToken = fmt.Sprintf("Bearer %s", tokenResponse.Token)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ addTokenAuthHeader(req, anonymousToken)
+ MakeRequest(t, req, http.StatusOK)
+ })
+
+ t.Run("User", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ resp := MakeRequest(t, req, http.StatusUnauthorized)
+
+ assert.ElementsMatch(t, authenticate, resp.Header().Values("WWW-Authenticate"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ tokenResponse := &TokenResponse{}
+ DecodeJSON(t, resp, &tokenResponse)
+
+ assert.NotEmpty(t, tokenResponse.Token)
+
+ userToken = fmt.Sprintf("Bearer %s", tokenResponse.Token)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusOK)
+ })
+ })
+
+ t.Run("DetermineSupport", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version"))
+ })
+
+ for _, image := range images {
+ t.Run(fmt.Sprintf("[Image:%s]", image), func(t *testing.T) {
+ url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image)
+
+ t.Run("UploadBlob/Monolithic", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url))
+ addTokenAuthHeader(req, anonymousToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, unknownDigest), bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, blobDigest), bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, container_model.UploadVersion)
+ assert.NoError(t, err)
+
+ pfs, err := packages_model.GetFilesByVersionID(db.DefaultContext, pv.ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+
+ pb, err := packages_model.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.EqualValues(t, len(blobContent), pb.Size)
+ })
+
+ t.Run("UploadBlob/Chunked", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusAccepted)
+
+ uuid := resp.Header().Get("Docker-Upload-Uuid")
+ assert.NotEmpty(t, uuid)
+
+ pbu, err := packages_model.GetBlobUploadByID(db.DefaultContext, uuid)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 0, pbu.BytesReceived)
+
+ uploadURL := resp.Header().Get("Location")
+ assert.NotEmpty(t, uploadURL)
+
+ req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:]+"000", bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:], bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+
+ req.Header.Set("Content-Range", "1-10")
+ MakeRequest(t, req, http.StatusRequestedRangeNotSatisfiable)
+
+ contentRange := fmt.Sprintf("0-%d", len(blobContent)-1)
+ req.Header.Set("Content-Range", contentRange)
+ resp = MakeRequest(t, req, http.StatusAccepted)
+
+ assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid"))
+ assert.Equal(t, contentRange, resp.Header().Get("Range"))
+
+ pbu, err = packages_model.GetBlobUploadByID(db.DefaultContext, uuid)
+ assert.NoError(t, err)
+ assert.EqualValues(t, len(blobContent), pbu.BytesReceived)
+
+ uploadURL = resp.Header().Get("Location")
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("%s?digest=%s", setting.AppURL+uploadURL[1:], blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp = MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ for _, tag := range tags {
+ t.Run(fmt.Sprintf("[Tag:%s]", tag), func(t *testing.T) {
+ t.Run("UploadManifest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, configDigest), strings.NewReader(configContent))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent))
+ addTokenAuthHeader(req, anonymousToken)
+ req.Header.Set("Content-Type", oci.MediaTypeDockerManifest)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeDockerManifest)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag)
+ assert.NoError(t, err)
+
+ pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv)
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Equal(t, image, pd.Package.Name)
+ assert.Equal(t, tag, pd.Version.Version)
+ assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
+ assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
+
+ assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
+ metadata := pd.Metadata.(*container_module.Metadata)
+ assert.Equal(t, container_module.TypeOCI, metadata.Type)
+ assert.Len(t, metadata.ImageLayers, 2)
+ assert.Empty(t, metadata.MultiArch)
+
+ assert.Len(t, pd.Files, 3)
+ for _, pfd := range pd.Files {
+ switch pfd.File.Name {
+ case container_model.ManifestFilename:
+ assert.True(t, pfd.File.IsLead)
+ assert.Equal(t, oci.MediaTypeDockerManifest, pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, manifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ case strings.Replace(configDigest, ":", "_", 1):
+ assert.False(t, pfd.File.IsLead)
+ assert.Equal(t, "application/vnd.docker.container.image.v1+json", pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, configDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ case strings.Replace(blobDigest, ":", "_", 1):
+ assert.False(t, pfd.File.IsLead)
+ assert.Equal(t, "application/vnd.docker.image.rootfs.diff.tar.gzip", pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, blobDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ default:
+ assert.Fail(t, "unknown file: %s", pfd.File.Name)
+ }
+ }
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusOK)
+
+ pv, err = packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 1, pv.DownloadCount)
+
+ // Overwrite existing tag should keep the download count
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeDockerManifest)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pv, err = packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 1, pv.DownloadCount)
+ })
+
+ t.Run("HeadManifest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/unknown-tag", url))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, tag))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(manifestContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ t.Run("GetManifest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/unknown-tag", url))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(manifestContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, oci.MediaTypeDockerManifest, resp.Header().Get("Content-Type"))
+ assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
+ assert.Equal(t, manifestContent, resp.Body.String())
+ })
+ })
+ }
+
+ t.Run("UploadUntaggedManifest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest), strings.NewReader(untaggedManifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeImageManifest)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, untaggedManifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest))
+ addTokenAuthHeader(req, userToken)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(untaggedManifestContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, untaggedManifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, untaggedManifestDigest)
+ assert.NoError(t, err)
+
+ pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv)
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Equal(t, image, pd.Package.Name)
+ assert.Equal(t, untaggedManifestDigest, pd.Version.Version)
+ assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
+ assert.False(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
+
+ assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
+
+ assert.Len(t, pd.Files, 3)
+ for _, pfd := range pd.Files {
+ if pfd.File.Name == container_model.ManifestFilename {
+ assert.True(t, pfd.File.IsLead)
+ assert.Equal(t, oci.MediaTypeImageManifest, pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, untaggedManifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ }
+ }
+ })
+
+ t.Run("UploadIndexManifest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, multiTag), strings.NewReader(indexManifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeImageIndex)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, indexManifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, multiTag)
+ assert.NoError(t, err)
+
+ pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv)
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Equal(t, image, pd.Package.Name)
+ assert.Equal(t, multiTag, pd.Version.Version)
+ assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
+ assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
+
+ assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference))
+
+ assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
+ metadata := pd.Metadata.(*container_module.Metadata)
+ assert.Equal(t, container_module.TypeOCI, metadata.Type)
+ assert.Contains(t, metadata.MultiArch, "linux/arm/v7")
+ assert.Equal(t, manifestDigest, metadata.MultiArch["linux/arm/v7"])
+ assert.Contains(t, metadata.MultiArch, "linux/arm64/v8")
+ assert.Equal(t, untaggedManifestDigest, metadata.MultiArch["linux/arm64/v8"])
+
+ assert.Len(t, pd.Files, 1)
+ assert.True(t, pd.Files[0].File.IsLead)
+ assert.Equal(t, oci.MediaTypeImageIndex, pd.Files[0].Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, indexManifestDigest, pd.Files[0].Properties.GetByName(container_module.PropertyDigest))
+ })
+
+ t.Run("UploadBlob/Mount", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s", url, unknownDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ t.Run("HeadBlob", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, unknownDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(blobContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ t.Run("GetBlob", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, unknownDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(blobContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ assert.Equal(t, blobContent, resp.Body.Bytes())
+ })
+
+ t.Run("GetTagList", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ cases := []struct {
+ URL string
+ ExpectedTags []string
+ ExpectedLink string
+ }{
+ {
+ URL: fmt.Sprintf("%s/tags/list", url),
+ ExpectedTags: []string{"latest", "main", "multi"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?n=0", url),
+ ExpectedTags: []string{},
+ ExpectedLink: "",
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?n=2", url),
+ ExpectedTags: []string{"latest", "main"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=2>; rel="next"`, user.Name, image),
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?last=main", url),
+ ExpectedTags: []string{"multi"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
+ ExpectedTags: []string{"main"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
+ },
+ }
+
+ for _, c := range cases {
+ req := NewRequest(t, "GET", c.URL)
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type TagList struct {
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+ }
+
+ tagList := &TagList{}
+ DecodeJSON(t, resp, &tagList)
+
+ assert.Equal(t, user.Name+"/"+image, tagList.Name)
+ assert.Equal(t, c.ExpectedTags, tagList.Tags)
+ assert.Equal(t, c.ExpectedLink, resp.Header().Get("Link"))
+ }
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s?type=container&q=%s", user.Name, image))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var apiPackages []*api.Package
+ DecodeJSON(t, resp, &apiPackages)
+ assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..."
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Blob", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("ManifestByDigest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("ManifestByTag", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/manifests/%s", url, multiTag))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, multiTag))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+ })
+ })
+ }
+
+ t.Run("OwnerNameChange", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ checkCatalog := func(owner string) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2/_catalog", setting.AppURL))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type RepositoryList struct {
+ Repositories []string `json:"repositories"`
+ }
+
+ repoList := &RepositoryList{}
+ DecodeJSON(t, resp, &repoList)
+
+ assert.Len(t, repoList.Repositories, len(images))
+ names := make([]string, 0, len(images))
+ for _, image := range images {
+ names = append(names, strings.ToLower(owner+"/"+image))
+ }
+ assert.ElementsMatch(t, names, repoList.Repositories)
+ }
+ }
+
+ t.Run(fmt.Sprintf("Catalog[%s]", user.LowerName), checkCatalog(user.LowerName))
+
+ session := loginUser(t, user.Name)
+
+ newOwnerName := "newUsername"
+
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": newOwnerName,
+ "email": "user2@example.com",
+ "language": "en-US",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
+
+ req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": user.Name,
+ "email": "user2@example.com",
+ "language": "en-US",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+ })
+}
diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go
new file mode 100644
index 0000000000..9fcd2cc797
--- /dev/null
+++ b/tests/integration/api_packages_generic_test.go
@@ -0,0 +1,194 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageGeneric(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ packageName := "te-st_pac.kage"
+ packageVersion := "1.0.3-te st"
+ filename := "fi-le_na.me"
+ content := []byte{1, 2, 3}
+
+ url := fmt.Sprintf("/api/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.Nil(t, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ t.Run("Exists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusConflict)
+ })
+
+ t.Run("Additional", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url+"/dummy.bin", bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ // Check deduplication
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2)
+ assert.Equal(t, pfs[0].BlobID, pfs[1].BlobID)
+ })
+
+ t.Run("InvalidParameter", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid+package name", packageVersion, filename), bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, "%20test ", filename), bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inval+id.na me"), bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ checkDownloadCount := func(count int64) {
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, count, pvs[0].DownloadCount)
+ }
+
+ checkDownloadCount(0)
+
+ req := NewRequest(t, "GET", url+"/"+filename)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+
+ checkDownloadCount(1)
+
+ req = NewRequest(t, "GET", url+"/dummy.bin")
+ MakeRequest(t, req, http.StatusOK)
+
+ checkDownloadCount(2)
+
+ t.Run("NotExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", url+"/not.found")
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("File", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", url+"/"+filename)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", url+"/"+filename)
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "GET", url+"/"+filename)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "DELETE", url+"/"+filename)
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ t.Run("RemovesVersion", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req = NewRequest(t, "DELETE", url+"/dummy.bin")
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+ })
+ })
+
+ t.Run("Version", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequest(t, "DELETE", url)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", url)
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+
+ req = NewRequest(t, "GET", url+"/"+filename)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "DELETE", url)
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+ })
+}
diff --git a/tests/integration/api_packages_helm_test.go b/tests/integration/api_packages_helm_test.go
new file mode 100644
index 0000000000..393bf3cbe2
--- /dev/null
+++ b/tests/integration/api_packages_helm_test.go
@@ -0,0 +1,167 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "archive/tar"
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ helm_module "code.gitea.io/gitea/modules/packages/helm"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/yaml.v2"
+)
+
+func TestPackageHelm(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ packageName := "test-chart"
+ packageVersion := "1.0.3"
+ packageAuthor := "KN4CK3R"
+ packageDescription := "Gitea Test Package"
+
+ filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion)
+
+ chartContent := `apiVersion: v2
+description: ` + packageDescription + `
+name: ` + packageName + `
+type: application
+version: ` + packageVersion + `
+maintainers:
+- name: ` + packageAuthor + `
+dependencies:
+- name: dep1
+ repository: https://example.com/
+ version: 1.0.0`
+
+ var buf bytes.Buffer
+ zw := gzip.NewWriter(&buf)
+ archive := tar.NewWriter(zw)
+ archive.WriteHeader(&tar.Header{
+ Name: fmt.Sprintf("%s/Chart.yaml", packageName),
+ Mode: 0o600,
+ Size: int64(len(chartContent)),
+ })
+ archive.Write([]byte(chartContent))
+ archive.Close()
+ zw.Close()
+ content := buf.Bytes()
+
+ url := fmt.Sprintf("/api/packages/%s/helm", user.Name)
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadURL := url + "/api/charts"
+
+ req := NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeHelm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &helm_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ req = NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ checkDownloadCount := func(count int64) {
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeHelm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, count, pvs[0].DownloadCount)
+ }
+
+ checkDownloadCount(0)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", url, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+
+ checkDownloadCount(1)
+ })
+
+ t.Run("Index", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/index.yaml", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type ChartVersion struct {
+ helm_module.Metadata `yaml:",inline"`
+ URLs []string `yaml:"urls"`
+ Created time.Time `yaml:"created,omitempty"`
+ Removed bool `yaml:"removed,omitempty"`
+ Digest string `yaml:"digest,omitempty"`
+ }
+
+ type ServerInfo struct {
+ ContextPath string `yaml:"contextPath,omitempty"`
+ }
+
+ type Index struct {
+ APIVersion string `yaml:"apiVersion"`
+ Entries map[string][]*ChartVersion `yaml:"entries"`
+ Generated time.Time `yaml:"generated,omitempty"`
+ ServerInfo *ServerInfo `yaml:"serverInfo,omitempty"`
+ }
+
+ var result Index
+ assert.NoError(t, yaml.NewDecoder(resp.Body).Decode(&result))
+ assert.NotEmpty(t, result.Entries)
+ assert.Contains(t, result.Entries, packageName)
+
+ cvs := result.Entries[packageName]
+ assert.Len(t, cvs, 1)
+
+ cv := cvs[0]
+ assert.Equal(t, packageName, cv.Name)
+ assert.Equal(t, packageVersion, cv.Version)
+ assert.Equal(t, packageDescription, cv.Description)
+ assert.Len(t, cv.Maintainers, 1)
+ assert.Equal(t, packageAuthor, cv.Maintainers[0].Name)
+ assert.Len(t, cv.Dependencies, 1)
+ assert.ElementsMatch(t, []string{fmt.Sprintf("%s%s/%s", setting.AppURL, url[1:], filename)}, cv.URLs)
+
+ assert.Equal(t, url, result.ServerInfo.ContextPath)
+ })
+}
diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go
new file mode 100644
index 0000000000..87d95557ce
--- /dev/null
+++ b/tests/integration/api_packages_maven_test.go
@@ -0,0 +1,218 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/maven"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageMaven(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ groupID := "com.gitea"
+ artifactID := "test-project"
+ packageName := groupID + "-" + artifactID
+ packageVersion := "1.0.1"
+ packageDescription := "Test Description"
+
+ root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID)
+ filename := fmt.Sprintf("%s-%s.jar", packageName, packageVersion)
+
+ putFile := func(t *testing.T, path, content string, expectedStatus int) {
+ req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, expectedStatus)
+ }
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusCreated)
+ putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusBadRequest)
+ putFile(t, "/maven-metadata.xml", "test", http.StatusOK)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Nil(t, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.False(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), pb.Size)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, []byte("test"), resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(0), pvs[0].DownloadCount)
+ })
+
+ t.Run("UploadVerifySHA1", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("Missmatch", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "test", http.StatusBadRequest)
+ })
+ t.Run("Valid", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", http.StatusOK)
+ })
+ })
+
+ pomContent := `<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <groupId>` + groupID + `</groupId>
+ <artifactId>` + artifactID + `</artifactId>
+ <version>` + packageVersion + `</version>
+ <description>` + packageDescription + `</description>
+</project>`
+
+ t.Run("UploadPOM", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.Nil(t, pd.Metadata)
+
+ putFile(t, fmt.Sprintf("/%s/%s.pom", packageVersion, filename), pomContent, http.StatusCreated)
+
+ pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err = packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.IsType(t, &maven.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageDescription, pd.Metadata.(*maven.Metadata).Description)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2)
+ for _, pf := range pfs {
+ if strings.HasSuffix(pf.Name, ".pom") {
+ assert.Equal(t, filename+".pom", pf.Name)
+ assert.True(t, pf.IsLead)
+ } else {
+ assert.False(t, pf.IsLead)
+ }
+ }
+ })
+
+ t.Run("DownloadPOM", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, []byte(pomContent), resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("DownloadChecksums", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ for key, checksum := range map[string]string{
+ "md5": "098f6bcd4621d373cade4e832627b4f6",
+ "sha1": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
+ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+ "sha512": "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
+ } {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, checksum, resp.Body.String())
+ }
+ })
+
+ t.Run("DownloadMetadata", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", root+"/maven-metadata.xml")
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ expectedMetadata := `<?xml version="1.0" encoding="UTF-8"?>` + "\n<metadata><groupId>com.gitea</groupId><artifactId>test-project</artifactId><versioning><release>1.0.1</release><latest>1.0.1</latest><versions><version>1.0.1</version></versions></versioning></metadata>"
+ assert.Equal(t, expectedMetadata, resp.Body.String())
+
+ for key, checksum := range map[string]string{
+ "md5": "6bee0cebaaa686d658adf3e7e16371a0",
+ "sha1": "8696abce499fe84d9ea93e5492abe7147e195b6c",
+ "sha256": "3f48322f81c4b2c3bb8649ae1e5c9801476162b520e1c2734ac06b2c06143208",
+ "sha512": "cb075aa2e2ef1a83cdc14dd1e08c505b72d633399b39e73a21f00f0deecb39a3e2c79f157c1163f8a3854828750706e0dec3a0f5e4778e91f8ec2cf351a855f2",
+ } {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, checksum, resp.Body.String())
+ }
+ })
+
+ t.Run("UploadSnapshot", func(t *testing.T) {
+ snapshotVersion := packageVersion + "-SNAPSHOT"
+
+ putFile(t, fmt.Sprintf("/%s/%s", snapshotVersion, filename), "test", http.StatusCreated)
+ putFile(t, "/maven-metadata.xml", "test", http.StatusOK)
+ putFile(t, fmt.Sprintf("/%s/maven-metadata.xml", snapshotVersion), "test", http.StatusCreated)
+ putFile(t, fmt.Sprintf("/%s/maven-metadata.xml", snapshotVersion), "test-overwrite", http.StatusCreated)
+ })
+}
diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go
new file mode 100644
index 0000000000..fe6cea1cb6
--- /dev/null
+++ b/tests/integration/api_packages_npm_test.go
@@ -0,0 +1,279 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/npm"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageNpm(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name)))
+
+ packageName := "@scope/test-package"
+ packageVersion := "1.0.1-pre"
+ packageTag := "latest"
+ packageTag2 := "release"
+ packageAuthor := "KN4CK3R"
+ packageDescription := "Test Description"
+
+ data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
+
+ buildUpload := func(version string) string {
+ return `{
+ "_id": "` + packageName + `",
+ "name": "` + packageName + `",
+ "description": "` + packageDescription + `",
+ "dist-tags": {
+ "` + packageTag + `": "` + version + `"
+ },
+ "versions": {
+ "` + version + `": {
+ "name": "` + packageName + `",
+ "version": "` + version + `",
+ "description": "` + packageDescription + `",
+ "author": {
+ "name": "` + packageAuthor + `"
+ },
+ "dist": {
+ "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==",
+ "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90"
+ }
+ }
+ },
+ "_attachments": {
+ "` + packageName + `-` + version + `.tgz": {
+ "data": "` + data + `"
+ }
+ }
+ }`
+ }
+
+ root := fmt.Sprintf("/api/packages/%s/npm/%s", user.Name, url.QueryEscape(packageName))
+ tagsRoot := fmt.Sprintf("/api/packages/%s/npm/-/package/%s/dist-tags", user.Name, url.QueryEscape(packageName))
+ filename := fmt.Sprintf("%s-%s.tgz", strings.Split(packageName, "/")[1], packageVersion)
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion)))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &npm.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+ assert.Len(t, pd.VersionProperties, 1)
+ assert.Equal(t, npm.TagProperty, pd.VersionProperties[0].Name)
+ assert.Equal(t, packageTag, pd.VersionProperties[0].Value)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(192), pb.Size)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion)))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename))
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ b, _ := base64.StdEncoding.DecodeString(data)
+ assert.Equal(t, b, resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("PackageMetadata", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/packages/%s/npm/%s", user.Name, "does-not-exist"))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", root)
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result npm.PackageMetadata
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, packageName, result.ID)
+ assert.Equal(t, packageName, result.Name)
+ assert.Equal(t, packageDescription, result.Description)
+ assert.Contains(t, result.DistTags, packageTag)
+ assert.Equal(t, packageVersion, result.DistTags[packageTag])
+ assert.Equal(t, packageAuthor, result.Author.Name)
+ assert.Contains(t, result.Versions, packageVersion)
+ pmv := result.Versions[packageVersion]
+ assert.Equal(t, fmt.Sprintf("%s@%s", packageName, packageVersion), pmv.ID)
+ assert.Equal(t, packageName, pmv.Name)
+ assert.Equal(t, packageDescription, pmv.Description)
+ assert.Equal(t, packageAuthor, pmv.Author.Name)
+ assert.Equal(t, "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", pmv.Dist.Integrity)
+ assert.Equal(t, "aaa7eaf852a948b0aa05afeda35b1badca155d90", pmv.Dist.Shasum)
+ assert.Equal(t, fmt.Sprintf("%s%s/-/%s/%s", setting.AppURL, root[1:], packageVersion, filename), pmv.Dist.Tarball)
+ })
+
+ t.Run("AddTag", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ test := func(t *testing.T, status int, tag, version string) {
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", tagsRoot, tag), strings.NewReader(`"`+version+`"`))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, status)
+ }
+
+ test(t, http.StatusBadRequest, "1.0", packageVersion)
+ test(t, http.StatusBadRequest, "v1.0", packageVersion)
+ test(t, http.StatusNotFound, packageTag2, "1.2")
+ test(t, http.StatusOK, packageTag, packageVersion)
+ test(t, http.StatusOK, packageTag2, packageVersion)
+ })
+
+ t.Run("ListTags", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", tagsRoot)
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string]string
+ DecodeJSON(t, resp, &result)
+
+ assert.Len(t, result, 2)
+ assert.Contains(t, result, packageTag)
+ assert.Equal(t, packageVersion, result[packageTag])
+ assert.Contains(t, result, packageTag2)
+ assert.Equal(t, packageVersion, result[packageTag2])
+ })
+
+ t.Run("PackageMetadataDistTags", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", root)
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result npm.PackageMetadata
+ DecodeJSON(t, resp, &result)
+
+ assert.Len(t, result.DistTags, 2)
+ assert.Contains(t, result.DistTags, packageTag)
+ assert.Equal(t, packageVersion, result.DistTags[packageTag])
+ assert.Contains(t, result.DistTags, packageTag2)
+ assert.Equal(t, packageVersion, result.DistTags[packageTag2])
+ })
+
+ t.Run("DeleteTag", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ test := func(t *testing.T, status int, tag string) {
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", tagsRoot, tag))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, status)
+ }
+
+ test(t, http.StatusBadRequest, "v1.0")
+ test(t, http.StatusBadRequest, "1.0")
+ test(t, http.StatusOK, "dummy")
+ test(t, http.StatusOK, packageTag2)
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion+"-dummy")))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequest(t, "PUT", root+"/-rev/dummy")
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "PUT", root+"/-rev/dummy")
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ t.Run("Version", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 2)
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ })
+
+ t.Run("Full", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ req := NewRequest(t, "DELETE", root+"/-rev/dummy")
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", root+"/-rev/dummy")
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 0)
+ })
+ })
+}
diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go
new file mode 100644
index 0000000000..87275feb3e
--- /dev/null
+++ b/tests/integration/api_packages_nuget_test.go
@@ -0,0 +1,393 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ nuget_module "code.gitea.io/gitea/modules/packages/nuget"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers/api/packages/nuget"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func addNuGetAPIKeyHeader(request *http.Request, token string) *http.Request {
+ request.Header.Set("X-NuGet-ApiKey", token)
+ return request
+}
+
+func TestPackageNuGet(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ token := getUserToken(t, user.Name)
+
+ packageName := "test.package"
+ packageVersion := "1.0.3"
+ packageAuthors := "KN4CK3R"
+ packageDescription := "Gitea Test Package"
+ symbolFilename := "test.pdb"
+ symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
+
+ var buf bytes.Buffer
+ archive := zip.NewWriter(&buf)
+ w, _ := archive.Create("package.nuspec")
+ w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
+ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>` + packageName + `</id>
+ <version>` + packageVersion + `</version>
+ <authors>` + packageAuthors + `</authors>
+ <description>` + packageDescription + `</description>
+ <group targetFramework=".NETStandard2.0">
+ <dependency id="Microsoft.CSharp" version="4.5.0" />
+ </group>
+ </metadata>
+ </package>`))
+ archive.Close()
+ content := buf.Bytes()
+
+ url := fmt.Sprintf("/api/packages/%s/nuget", user.Name)
+
+ t.Run("ServiceIndex", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
+ req = addNuGetAPIKeyHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.ServiceIndexResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, "3.0.0", result.Version)
+ assert.NotEmpty(t, result.Resources)
+
+ root := setting.AppURL + url[1:]
+ for _, r := range result.Resources {
+ switch r.Type {
+ case "SearchQueryService":
+ fallthrough
+ case "SearchQueryService/3.0.0-beta":
+ fallthrough
+ case "SearchQueryService/3.0.0-rc":
+ assert.Equal(t, root+"/query", r.ID)
+ case "RegistrationsBaseUrl":
+ fallthrough
+ case "RegistrationsBaseUrl/3.0.0-beta":
+ fallthrough
+ case "RegistrationsBaseUrl/3.0.0-rc":
+ assert.Equal(t, root+"/registration", r.ID)
+ case "PackageBaseAddress/3.0.0":
+ assert.Equal(t, root+"/package", r.ID)
+ case "PackagePublish/2.0.0":
+ assert.Equal(t, root, r.ID)
+ }
+ }
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ t.Run("DependencyPackage", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusConflict)
+ })
+
+ t.Run("SymbolPackage", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ createPackage := func(id, packageType string) io.Reader {
+ var buf bytes.Buffer
+ archive := zip.NewWriter(&buf)
+
+ w, _ := archive.Create("package.nuspec")
+ w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
+ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>` + id + `</id>
+ <version>` + packageVersion + `</version>
+ <authors>` + packageAuthors + `</authors>
+ <description>` + packageDescription + `</description>
+ <packageTypes><packageType name="` + packageType + `" /></packageTypes>
+ </metadata>
+ </package>`))
+
+ w, _ = archive.Create(symbolFilename)
+ b, _ := base64.StdEncoding.DecodeString(`QlNKQgEAAQAAAAAADAAAAFBEQiB2MS4wAAAAAAAABgB8AAAAWAAAACNQZGIAAAAA1AAAAAgBAAAj
+fgAA3AEAAAQAAAAjU3RyaW5ncwAAAADgAQAABAAAACNVUwDkAQAAMAAAACNHVUlEAAAAFAIAACgB
+AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
+ w.Write(b)
+
+ archive.Close()
+ return &buf
+ }
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage("unknown-package", "SymbolsPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "DummyPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 3)
+ for _, pf := range pfs {
+ switch pf.Name {
+ case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
+ case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
+ assert.False(t, pf.IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(616), pb.Size)
+ case symbolFilename:
+ assert.False(t, pf.IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(160), pb.Size)
+
+ pps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID)
+ assert.NoError(t, err)
+ assert.Len(t, pps, 1)
+ assert.Equal(t, nuget_module.PropertySymbolID, pps[0].Name)
+ assert.Equal(t, symbolID, pps[0].Value)
+ default:
+ assert.Fail(t, "unexpected file: %v", pf.Name)
+ }
+ }
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusConflict)
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ checkDownloadCount := func(count int64) {
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, count, pvs[0].DownloadCount)
+ }
+
+ checkDownloadCount(0)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+
+ checkDownloadCount(1)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkDownloadCount(1)
+
+ t.Run("Symbol", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/gitea.pdb", url, symbolFilename, symbolID))
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/%s", url, symbolFilename, "00000000000000000000000000000000", symbolFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/%s", url, symbolFilename, symbolID, symbolFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkDownloadCount(1)
+ })
+ })
+
+ t.Run("SearchService", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Skip int
+ Take int
+ ExpectedTotal int64
+ ExpectedResults int
+ }{
+ {"", 0, 0, 1, 1},
+ {"", 0, 10, 1, 1},
+ {"gitea", 0, 10, 0, 0},
+ {"test", 0, 10, 1, 1},
+ {"test", 1, 10, 1, 0},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.SearchResultResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i)
+ assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i)
+ }
+ })
+
+ t.Run("RegistrationService", func(t *testing.T) {
+ indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName)
+ leafURL := fmt.Sprintf("%s%s/registration/%s/%s.json", setting.AppURL, url[1:], packageName, packageVersion)
+ contentURL := fmt.Sprintf("%s%s/package/%s/%s/%s.%s.nupkg", setting.AppURL, url[1:], packageName, packageVersion, packageName, packageVersion)
+
+ t.Run("RegistrationIndex", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/index.json", url, packageName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.RegistrationIndexResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, indexURL, result.RegistrationIndexURL)
+ assert.Equal(t, 1, result.Count)
+ assert.Len(t, result.Pages, 1)
+ assert.Equal(t, indexURL, result.Pages[0].RegistrationPageURL)
+ assert.Equal(t, packageVersion, result.Pages[0].Lower)
+ assert.Equal(t, packageVersion, result.Pages[0].Upper)
+ assert.Equal(t, 1, result.Pages[0].Count)
+ assert.Len(t, result.Pages[0].Items, 1)
+ assert.Equal(t, packageName, result.Pages[0].Items[0].CatalogEntry.ID)
+ assert.Equal(t, packageVersion, result.Pages[0].Items[0].CatalogEntry.Version)
+ assert.Equal(t, packageAuthors, result.Pages[0].Items[0].CatalogEntry.Authors)
+ assert.Equal(t, packageDescription, result.Pages[0].Items[0].CatalogEntry.Description)
+ assert.Equal(t, leafURL, result.Pages[0].Items[0].CatalogEntry.CatalogLeafURL)
+ assert.Equal(t, contentURL, result.Pages[0].Items[0].CatalogEntry.PackageContentURL)
+ })
+
+ t.Run("RegistrationLeaf", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.RegistrationLeafResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, leafURL, result.RegistrationLeafURL)
+ assert.Equal(t, contentURL, result.PackageContentURL)
+ assert.Equal(t, indexURL, result.RegistrationIndexURL)
+ })
+ })
+
+ t.Run("PackageService", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.PackageVersionsResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Len(t, result.Versions, 1)
+ assert.Equal(t, packageVersion, result.Versions[0])
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+ })
+
+ t.Run("DownloadNotExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("DeleteNotExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s", url, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+}
diff --git a/tests/integration/api_packages_pub_test.go b/tests/integration/api_packages_pub_test.go
new file mode 100644
index 0000000000..9e4ce63fa1
--- /dev/null
+++ b/tests/integration/api_packages_pub_test.go
@@ -0,0 +1,180 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "archive/tar"
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ pub_module "code.gitea.io/gitea/modules/packages/pub"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackagePub(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ token := "Bearer " + getUserToken(t, user.Name)
+
+ packageName := "test_package"
+ packageVersion := "1.0.1"
+ packageDescription := "Test Description"
+
+ filename := fmt.Sprintf("%s.tar.gz", packageVersion)
+
+ pubspecContent := `name: ` + packageName + `
+version: ` + packageVersion + `
+description: ` + packageDescription
+
+ var buf bytes.Buffer
+ zw := gzip.NewWriter(&buf)
+ archive := tar.NewWriter(zw)
+ archive.WriteHeader(&tar.Header{
+ Name: "pubspec.yaml",
+ Mode: 0o600,
+ Size: int64(len(pubspecContent)),
+ })
+ archive.Write([]byte(pubspecContent))
+ archive.Close()
+ zw.Close()
+ content := buf.Bytes()
+
+ root := fmt.Sprintf("/api/packages/%s/pub", user.Name)
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadURL := root + "/api/packages/versions/new"
+
+ req := NewRequest(t, "GET", uploadURL)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "GET", uploadURL)
+ addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type UploadRequest struct {
+ URL string `json:"url"`
+ Fields map[string]string `json:"fields"`
+ }
+
+ var result UploadRequest
+ DecodeJSON(t, resp, &result)
+
+ assert.Empty(t, result.Fields)
+
+ uploadFile := func(t *testing.T, url string, content []byte, expectedStatus int) *httptest.ResponseRecorder {
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, _ := writer.CreateFormFile("file", "dummy.tar.gz")
+ _, _ = io.Copy(part, bytes.NewReader(content))
+
+ _ = writer.Close()
+
+ req := NewRequestWithBody(t, "POST", url, body)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ addTokenAuthHeader(req, token)
+ return MakeRequest(t, req, expectedStatus)
+ }
+
+ resp = uploadFile(t, result.URL, content, http.StatusNoContent)
+
+ req = NewRequest(t, "GET", resp.Header().Get("Location"))
+ addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePub)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &pub_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ resp = uploadFile(t, result.URL, content, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/api/packages/%s/%s", root, packageName, packageVersion))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type VersionMetadata struct {
+ Version string `json:"version"`
+ ArchiveURL string `json:"archive_url"`
+ Published time.Time `json:"published"`
+ Pubspec interface{} `json:"pubspec,omitempty"`
+ }
+
+ var result VersionMetadata
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, packageVersion, result.Version)
+ assert.NotNil(t, result.Pubspec)
+
+ req = NewRequest(t, "GET", result.ArchiveURL)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+ })
+
+ t.Run("EnumeratePackageVersions", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/api/packages/%s", root, packageName))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type VersionMetadata struct {
+ Version string `json:"version"`
+ ArchiveURL string `json:"archive_url"`
+ Published time.Time `json:"published"`
+ Pubspec interface{} `json:"pubspec,omitempty"`
+ }
+
+ type PackageVersions struct {
+ Name string `json:"name"`
+ Latest *VersionMetadata `json:"latest"`
+ Versions []*VersionMetadata `json:"versions"`
+ }
+
+ var result PackageVersions
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, packageName, result.Name)
+ assert.NotNil(t, result.Latest)
+ assert.Len(t, result.Versions, 1)
+ assert.Equal(t, result.Latest.Version, result.Versions[0].Version)
+ assert.Equal(t, packageVersion, result.Latest.Version)
+ assert.NotNil(t, result.Latest.Pubspec)
+ })
+}
diff --git a/tests/integration/api_packages_pypi_test.go b/tests/integration/api_packages_pypi_test.go
new file mode 100644
index 0000000000..32b3304ca7
--- /dev/null
+++ b/tests/integration/api_packages_pypi_test.go
@@ -0,0 +1,182 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "regexp"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/pypi"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackagePyPI(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ packageName := "test-package"
+ packageVersion := "1.0.1"
+ packageAuthor := "KN4CK3R"
+ packageDescription := "Test Description"
+
+ content := "test"
+ hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
+
+ root := fmt.Sprintf("/api/packages/%s/pypi", user.Name)
+
+ uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, _ := writer.CreateFormFile("content", filename)
+ _, _ = io.Copy(part, strings.NewReader(content))
+
+ writer.WriteField("name", packageName)
+ writer.WriteField("version", packageVersion)
+ writer.WriteField("author", packageAuthor)
+ writer.WriteField("summary", packageDescription)
+ writer.WriteField("description", packageDescription)
+ writer.WriteField("sha256_digest", hashSHA256)
+ writer.WriteField("requires_python", "3.6")
+
+ _ = writer.Close()
+
+ req := NewRequestWithBody(t, "POST", root, body)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, expectedStatus)
+ }
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ filename := "test.whl"
+ uploadFile(t, filename, content, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), pb.Size)
+ })
+
+ t.Run("UploadAddFile", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ filename := "test.tar.gz"
+ uploadFile(t, filename, content, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2)
+
+ pf, err := packages.GetFileForVersionByName(db.DefaultContext, pvs[0].ID, filename, packages.EmptyFileKey)
+ assert.NoError(t, err)
+ assert.Equal(t, filename, pf.Name)
+ assert.True(t, pf.IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), pb.Size)
+ })
+
+ t.Run("UploadHashMismatch", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ filename := "test2.whl"
+ uploadFile(t, filename, "dummy", http.StatusBadRequest)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadFile(t, "test.whl", content, http.StatusBadRequest)
+ uploadFile(t, "test.tar.gz", content, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ downloadFile := func(filename string) {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/files/%s/%s/%s", root, packageName, packageVersion, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, []byte(content), resp.Body.Bytes())
+ }
+
+ downloadFile("test.whl")
+ downloadFile("test.tar.gz")
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(2), pvs[0].DownloadCount)
+ })
+
+ t.Run("PackageMetadata", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/simple/%s", root, packageName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ nodes := htmlDoc.doc.Find("a").Nodes
+ assert.Len(t, nodes, 2)
+
+ hrefMatcher := regexp.MustCompile(fmt.Sprintf(`%s/files/%s/%s/test\..+#sha256-%s`, root, packageName, packageVersion, hashSHA256))
+
+ for _, a := range nodes {
+ for _, att := range a.Attr {
+ switch att.Key {
+ case "href":
+ assert.Regexp(t, hrefMatcher, att.Val)
+ case "data-requires-python":
+ assert.Equal(t, "3.6", att.Val)
+ default:
+ t.Fail()
+ }
+ }
+ }
+ })
+}
diff --git a/tests/integration/api_packages_rubygems_test.go b/tests/integration/api_packages_rubygems_test.go
new file mode 100644
index 0000000000..6cf5af710b
--- /dev/null
+++ b/tests/integration/api_packages_rubygems_test.go
@@ -0,0 +1,227 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "mime/multipart"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/rubygems"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageRubyGems(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ packageName := "gitea"
+ packageVersion := "1.0.5"
+ packageFilename := "gitea-1.0.5.gem"
+
+ gemContent, _ := base64.StdEncoding.DecodeString(`bWV0YWRhdGEuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAw
+MAAwMDAwMDAwADAwMDAwMDAxMDQxADE0MTEwNzcyMzY2ADAxMzQ0MQAgMAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAw
+MDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf
+iwgA9vQjYQID1VVNb9QwEL37V5he9pRsmlJAFlQckCoOXAriQIUix5nNmsYf2JOqKwS/nYmz2d3Q
+qqCCKpFdadfjmfdm5nmcLMv4k9DXm6Wrv4BCcQ5GiPcelF5pJVE7y6w0IHirESS7hhDJJu4I+jhu
+Mc53Tsd5kZ8y30lcuWAEH2KY7HHtQhQs4+cJkwwuwNdeB6JhtbaNDoLTL1MQsFJrqQnr8jNrJJJH
+WZTHWfEiK094UYj0zYvp4Z9YAx5sA1ZpSCS3M30zeWwo2bG60FvUBjIKJts2GwMW76r0Yr9NzjN3
+YhwsGX2Ozl4dpcWwvK9d43PQtDIv9igvHwSyIIwFmXHjqTqxLY8MPkCADmQk80p2EfZ6VbM6/ue6
+/1D0Bq7/qeA/zh6W82leHmhFWUHn/JbsEfT6q7QbiCpoj8l0QcEUFLmX6kq2wBEiMjBSd+Pwt7T5
+Ot0kuXYMbkD1KOuOBnWYb7hBsAP4bhlkFRqnqpWefMZ/pHCn6+WIFGq2dgY8EQq+RvRRLJcTyZJ1
+WhHqGPTu7QdmACXdJFLwb9+ZdxErbSPKrqsMxJhAWCJ1qaqRdtu6yktcT/STsamG0qp7rsa5EL/K
+MBua30uw4ynzExqYWRJDfx8/kQWN3PwsDh2jYLr1W+pZcAmCs9splvnz/Flesqhbq21bXcGG/OLh
++2fv/JTF3hgZyCW9OaZjxoZjdnBGfgKpxZyJ1QYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGF0
+YS50YXIuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAwMAAw
+MDAwMDAwADAwMDAwMDAwMjQyADE0MTEwNzcyMzY2ADAxMzM2MQAgMAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfiwgA
+9vQjYQID7M/NCsMgDABgz32KrA/QxersK/Q17ExXIcyhlr7+HLv1sJ02KPhBCPk5JOyn881nsl2c
+xI+gRDRaC3zbZ8RBCamlxGHolTFlX11kLwDFH6wp21hO2RYi/rD3bb5/7iCubFOCMbBtABzNkIjn
+bvGlAnisOUE7EnOALUR2p7b06e6aV4iqqqrquJ4AAAD//wMA+sA/NQAIAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNoZWNr
+c3Vtcy55YW1sLmd6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwNDQ0ADAwMDAwMDAAMDAw
+MDAwMAAwMDAwMDAwMDQ1MAAxNDExMDc3MjM2NgAwMTQ2MTIAIDAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdXN0YXIAMDB3aGVlbAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAwMDAwMAAwMDAwMDAwAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4sIAPb0
+I2ECA2WQOa4UQAxE8znFXGCQ21vbPyMj5wRuL0Qk6EecnmZCyKyy9FSvXq/X4/u3ryj68Xg+f/Zn
+VHzGlx+/P57qvU4XxWalBKftSXOgCjNYkdRycrC5Axem+W4HqS12PNEv7836jF9vnlHxwSyxKY+y
+go0cPblyHzkrZ4HF1GSVhe7mOOoasXNk2fnbUxb+19Pp9tobD/QlJKMX7y204PREh6nQ5hG9Alw6
+x4TnmtA+aekGfm6wAseog2LSgpR4Q7cYnAH3K4qAQa6A6JCC1gpuY7P+9YxE5SZ+j0eVGbaBTwBQ
+iIqRUyyzLCoFCBdYNWxniapTavD97blXTzFvgoVoAsKBAtlU48cdaOmeZDpwV01OtcGwjscfeUrY
+B9QBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`)
+
+ root := fmt.Sprintf("/api/packages/%s/rubygems", user.Name)
+
+ uploadFile := func(t *testing.T, expectedStatus int) {
+ req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/api/v1/gems", root), bytes.NewReader(gemContent))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, expectedStatus)
+ }
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadFile(t, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &rubygems.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, packageFilename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4608), pb.Size)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ uploadFile(t, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/gems/%s", root, packageFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, gemContent, resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("DownloadGemspec", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/quick/Marshal.4.8/%sspec.rz", root, packageFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ b, _ := base64.StdEncoding.DecodeString(`eJxi4Si1EndPzbWyCi5ITc5My0xOLMnMz2M8zMIRLeGpxGWsZ6RnzGbF5hqSyempxJWeWZKayGbN
+EBJqJQjWFZZaVJyZnxfN5qnEZahnoGcKkjTwVBJyB6lUKEhMzk5MTwULGngqcRaVJlWCONEMBp5K
+DGAWSKc7zFhPJamg0qRK99TcYphehZLU4hKInFhGSUlBsZW+PtgZepn5+iDxECRzDUDGcfh6hoA4
+gAAAAP//MS06Gw==`)
+ assert.Equal(t, b, resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("EnumeratePackages", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ enumeratePackages := func(t *testing.T, endpoint string, expectedContent []byte) {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", root, endpoint))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, expectedContent, resp.Body.Bytes())
+ }
+
+ b, _ := base64.StdEncoding.DecodeString(`H4sICAAAAAAA/3NwZWNzLjQuOABi4Yhmi+bwVOJKzyxJTWSzYnMNCbUSdE/NtbIKSy0qzszPi2bzVOIy1DPQM2WzZgjxVOIsKk2qBDEBAQAA///xOEYKOwAAAA==`)
+ enumeratePackages(t, "specs.4.8.gz", b)
+ b, _ = base64.StdEncoding.DecodeString(`H4sICAAAAAAA/2xhdGVzdF9zcGVjcy40LjgAYuGIZovm8FTiSs8sSU1ks2JzDQm1EnRPzbWyCkstKs7Mz4tm81TiMtQz0DNls2YI8VTiLCpNqgQxAQEAAP//8ThGCjsAAAA=`)
+ enumeratePackages(t, "latest_specs.4.8.gz", b)
+ b, _ = base64.StdEncoding.DecodeString(`H4sICAAAAAAA/3ByZXJlbGVhc2Vfc3BlY3MuNC44AGLhiGYABAAA//9snXr5BAAAAA==`)
+ enumeratePackages(t, "prerelease_specs.4.8.gz", b)
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ body := bytes.Buffer{}
+ writer := multipart.NewWriter(&body)
+ writer.WriteField("gem_name", packageName)
+ writer.WriteField("version", packageVersion)
+ writer.Close()
+
+ req := NewRequestWithBody(t, "DELETE", fmt.Sprintf("%s/api/v1/gems/yank", root), &body)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+ })
+}
diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go
new file mode 100644
index 0000000000..86d81994d4
--- /dev/null
+++ b/tests/integration/api_packages_test.go
@@ -0,0 +1,168 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ packages_model "code.gitea.io/gitea/models/packages"
+ container_model "code.gitea.io/gitea/models/packages/container"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ packages_service "code.gitea.io/gitea/services/packages"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageAPI(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ packageName := "test-package"
+ packageVersion := "1.0.3"
+ filename := "file.bin"
+
+ url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, filename)
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{}))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ t.Run("ListPackages", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s?token=%s", user.Name, token))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var apiPackages []*api.Package
+ DecodeJSON(t, resp, &apiPackages)
+
+ assert.Len(t, apiPackages, 1)
+ assert.Equal(t, string(packages_model.TypeGeneric), apiPackages[0].Type)
+ assert.Equal(t, packageName, apiPackages[0].Name)
+ assert.Equal(t, packageVersion, apiPackages[0].Version)
+ assert.NotNil(t, apiPackages[0].Creator)
+ assert.Equal(t, user.Name, apiPackages[0].Creator.UserName)
+ })
+
+ t.Run("GetPackage", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/dummy/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var p *api.Package
+ DecodeJSON(t, resp, &p)
+
+ assert.Equal(t, string(packages_model.TypeGeneric), p.Type)
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageVersion, p.Version)
+ assert.NotNil(t, p.Creator)
+ assert.Equal(t, user.Name, p.Creator.UserName)
+
+ t.Run("RepositoryLink", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ p, err := packages_model.GetPackageByName(db.DefaultContext, user.ID, packages_model.TypeGeneric, packageName)
+ assert.NoError(t, err)
+
+ // no repository link
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var ap1 *api.Package
+ DecodeJSON(t, resp, &ap1)
+ assert.Nil(t, ap1.Repository)
+
+ // link to public repository
+ assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 1))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ var ap2 *api.Package
+ DecodeJSON(t, resp, &ap2)
+ assert.NotNil(t, ap2.Repository)
+ assert.EqualValues(t, 1, ap2.Repository.ID)
+
+ // link to private repository
+ assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 2))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ var ap3 *api.Package
+ DecodeJSON(t, resp, &ap3)
+ assert.Nil(t, ap3.Repository)
+
+ assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, 2))
+ })
+ })
+
+ t.Run("ListPackageFiles", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/dummy/%s/%s/files?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s/files?token=%s", user.Name, packageName, packageVersion, token))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var files []*api.PackageFile
+ DecodeJSON(t, resp, &files)
+
+ assert.Len(t, files, 1)
+ assert.Equal(t, int64(0), files[0].Size)
+ assert.Equal(t, filename, files[0].Name)
+ assert.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", files[0].HashMD5)
+ assert.Equal(t, "da39a3ee5e6b4b0d3255bfef95601890afd80709", files[0].HashSHA1)
+ assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", files[0].HashSHA256)
+ assert.Equal(t, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", files[0].HashSHA512)
+ })
+
+ t.Run("DeletePackage", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/dummy/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNoContent)
+ })
+}
+
+func TestPackageCleanup(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ time.Sleep(time.Second)
+
+ pbs, err := packages_model.FindExpiredUnreferencedBlobs(db.DefaultContext, time.Duration(0))
+ assert.NoError(t, err)
+ assert.NotEmpty(t, pbs)
+
+ _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, 2, packages_model.TypeContainer, "test", container_model.UploadVersion)
+ assert.NoError(t, err)
+
+ err = packages_service.Cleanup(nil, time.Duration(0))
+ assert.NoError(t, err)
+
+ pbs, err = packages_model.FindExpiredUnreferencedBlobs(db.DefaultContext, time.Duration(0))
+ assert.NoError(t, err)
+ assert.Empty(t, pbs)
+
+ _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, 2, packages_model.TypeContainer, "test", container_model.UploadVersion)
+ assert.ErrorIs(t, err, packages_model.ErrPackageNotExist)
+}
diff --git a/tests/integration/api_packages_vagrant_test.go b/tests/integration/api_packages_vagrant_test.go
new file mode 100644
index 0000000000..1d2952e1a2
--- /dev/null
+++ b/tests/integration/api_packages_vagrant_test.go
@@ -0,0 +1,171 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "archive/tar"
+ "bytes"
+ "compress/gzip"
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
+ vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageVagrant(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ token := "Bearer " + getUserToken(t, user.Name)
+
+ packageName := "test_package"
+ packageVersion := "1.0.1"
+ packageDescription := "Test Description"
+ packageProvider := "virtualbox"
+
+ filename := fmt.Sprintf("%s.box", packageProvider)
+
+ infoContent, _ := json.Marshal(map[string]string{
+ "description": packageDescription,
+ })
+
+ var buf bytes.Buffer
+ zw := gzip.NewWriter(&buf)
+ archive := tar.NewWriter(zw)
+ archive.WriteHeader(&tar.Header{
+ Name: "info.json",
+ Mode: 0o600,
+ Size: int64(len(infoContent)),
+ })
+ archive.Write(infoContent)
+ archive.Close()
+ zw.Close()
+ content := buf.Bytes()
+
+ root := fmt.Sprintf("/api/packages/%s/vagrant", user.Name)
+
+ t.Run("Authenticate", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ authenticateURL := fmt.Sprintf("%s/authenticate", root)
+
+ req := NewRequest(t, "GET", authenticateURL)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "GET", authenticateURL)
+ addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+ })
+
+ boxURL := fmt.Sprintf("%s/%s", root, packageName)
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "HEAD", boxURL)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ uploadURL := fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
+ addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequest(t, "HEAD", boxURL)
+ resp := MakeRequest(t, req, http.StatusOK)
+ assert.True(t, strings.HasPrefix(resp.HeaderMap.Get("Content-Type"), "application/json"))
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeVagrant)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &vagrant_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
+ addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusConflict)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", boxURL, packageVersion, filename))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+ })
+
+ t.Run("EnumeratePackageVersions", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", boxURL)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type providerData struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ Checksum string `json:"checksum"`
+ ChecksumType string `json:"checksum_type"`
+ }
+
+ type versionMetadata struct {
+ Version string `json:"version"`
+ Status string `json:"status"`
+ DescriptionHTML string `json:"description_html,omitempty"`
+ DescriptionMarkdown string `json:"description_markdown,omitempty"`
+ Providers []*providerData `json:"providers"`
+ }
+
+ type packageMetadata struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ ShortDescription string `json:"short_description,omitempty"`
+ Versions []*versionMetadata `json:"versions"`
+ }
+
+ var result packageMetadata
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, packageName, result.Name)
+ assert.Equal(t, packageDescription, result.Description)
+ assert.Len(t, result.Versions, 1)
+ version := result.Versions[0]
+ assert.Equal(t, packageVersion, version.Version)
+ assert.Equal(t, "active", version.Status)
+ assert.Len(t, version.Providers, 1)
+ provider := version.Providers[0]
+ assert.Equal(t, packageProvider, provider.Name)
+ assert.Equal(t, "sha512", provider.ChecksumType)
+ assert.Equal(t, "259bebd6160acad695016d22a45812e26f187aaf78e71a4c23ee3201528346293f991af3468a8c6c5d2a21d7d9e1bdc1bf79b87110b2fddfcc5a0d45963c7c30", provider.Checksum)
+ })
+}
diff --git a/tests/integration/api_private_serv_test.go b/tests/integration/api_private_serv_test.go
new file mode 100644
index 0000000000..6fd6d616db
--- /dev/null
+++ b/tests/integration/api_private_serv_test.go
@@ -0,0 +1,154 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "net/url"
+ "testing"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/perm"
+ "code.gitea.io/gitea/modules/private"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIPrivateNoServ(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ key, user, err := private.ServNoCommand(ctx, 1)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(2), user.ID)
+ assert.Equal(t, "user2", user.Name)
+ assert.Equal(t, int64(1), key.ID)
+ assert.Equal(t, "user2@localhost", key.Name)
+
+ deployKey, err := asymkey_model.AddDeployKey(1, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
+ assert.NoError(t, err)
+
+ key, user, err = private.ServNoCommand(ctx, deployKey.KeyID)
+ assert.NoError(t, err)
+ assert.Empty(t, user)
+ assert.Equal(t, deployKey.KeyID, key.ID)
+ assert.Equal(t, "test-deploy", key.Name)
+ })
+}
+
+func TestAPIPrivateServ(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Can push to a repo we own
+ results, err := private.ServCommand(ctx, 1, "user2", "repo1", perm.AccessModeWrite, "git-upload-pack", "")
+ assert.NoError(t, err)
+ assert.False(t, results.IsWiki)
+ assert.Zero(t, results.DeployKeyID)
+ assert.Equal(t, int64(1), results.KeyID)
+ assert.Equal(t, "user2@localhost", results.KeyName)
+ assert.Equal(t, "user2", results.UserName)
+ assert.Equal(t, int64(2), results.UserID)
+ assert.Equal(t, "user2", results.OwnerName)
+ assert.Equal(t, "repo1", results.RepoName)
+ assert.Equal(t, int64(1), results.RepoID)
+
+ // Cannot push to a private repo we're not associated with
+ results, err = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "")
+ assert.Error(t, err)
+ assert.Empty(t, results)
+
+ // Cannot pull from a private repo we're not associated with
+ results, err = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "")
+ assert.Error(t, err)
+ assert.Empty(t, results)
+
+ // Can pull from a public repo we're not associated with
+ results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
+ assert.NoError(t, err)
+ assert.False(t, results.IsWiki)
+ assert.Zero(t, results.DeployKeyID)
+ assert.Equal(t, int64(1), results.KeyID)
+ assert.Equal(t, "user2@localhost", results.KeyName)
+ assert.Equal(t, "user2", results.UserName)
+ assert.Equal(t, int64(2), results.UserID)
+ assert.Equal(t, "user15", results.OwnerName)
+ assert.Equal(t, "big_test_public_1", results.RepoName)
+ assert.Equal(t, int64(17), results.RepoID)
+
+ // Cannot push to a public repo we're not associated with
+ results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeWrite, "git-upload-pack", "")
+ assert.Error(t, err)
+ assert.Empty(t, results)
+
+ // Add reading deploy key
+ deployKey, err := asymkey_model.AddDeployKey(19, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
+ assert.NoError(t, err)
+
+ // Can pull from repo we're a deploy key for
+ results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "")
+ assert.NoError(t, err)
+ assert.False(t, results.IsWiki)
+ assert.NotZero(t, results.DeployKeyID)
+ assert.Equal(t, deployKey.KeyID, results.KeyID)
+ assert.Equal(t, "test-deploy", results.KeyName)
+ assert.Equal(t, "user15", results.UserName)
+ assert.Equal(t, int64(15), results.UserID)
+ assert.Equal(t, "user15", results.OwnerName)
+ assert.Equal(t, "big_test_private_1", results.RepoName)
+ assert.Equal(t, int64(19), results.RepoID)
+
+ // Cannot push to a private repo with reading key
+ results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "")
+ assert.Error(t, err)
+ assert.Empty(t, results)
+
+ // Cannot pull from a private repo we're not associated with
+ results, err = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
+ assert.Error(t, err)
+ assert.Empty(t, results)
+
+ // Cannot pull from a public repo we're not associated with
+ results, err = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
+ assert.Error(t, err)
+ assert.Empty(t, results)
+
+ // Add writing deploy key
+ deployKey, err = asymkey_model.AddDeployKey(20, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
+ assert.NoError(t, err)
+
+ // Cannot push to a private repo with reading key
+ results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "")
+ assert.Error(t, err)
+ assert.Empty(t, results)
+
+ // Can pull from repo we're a writing deploy key for
+ results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
+ assert.NoError(t, err)
+ assert.False(t, results.IsWiki)
+ assert.NotZero(t, results.DeployKeyID)
+ assert.Equal(t, deployKey.KeyID, results.KeyID)
+ assert.Equal(t, "test-deploy", results.KeyName)
+ assert.Equal(t, "user15", results.UserName)
+ assert.Equal(t, int64(15), results.UserID)
+ assert.Equal(t, "user15", results.OwnerName)
+ assert.Equal(t, "big_test_private_2", results.RepoName)
+ assert.Equal(t, int64(20), results.RepoID)
+
+ // Can push to repo we're a writing deploy key for
+ results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeWrite, "git-upload-pack", "")
+ assert.NoError(t, err)
+ assert.False(t, results.IsWiki)
+ assert.NotZero(t, results.DeployKeyID)
+ assert.Equal(t, deployKey.KeyID, results.KeyID)
+ assert.Equal(t, "test-deploy", results.KeyName)
+ assert.Equal(t, "user15", results.UserName)
+ assert.Equal(t, int64(15), results.UserID)
+ assert.Equal(t, "user15", results.OwnerName)
+ assert.Equal(t, "big_test_private_2", results.RepoName)
+ assert.Equal(t, int64(20), results.RepoID)
+ })
+}
diff --git a/tests/integration/api_pull_commits_test.go b/tests/integration/api_pull_commits_test.go
new file mode 100644
index 0000000000..aa58f44bbe
--- /dev/null
+++ b/tests/integration/api_pull_commits_test.go
@@ -0,0 +1,41 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIPullCommits(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
+ assert.NoError(t, pullIssue.LoadIssue())
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.HeadRepoID})
+
+ session := loginUser(t, "user2")
+ req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/commits", repo.OwnerName, repo.Name, pullIssue.Index)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var commits []*api.Commit
+ DecodeJSON(t, resp, &commits)
+
+ if !assert.Len(t, commits, 2) {
+ return
+ }
+
+ assert.Equal(t, "5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commits[0].SHA)
+ assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", commits[1].SHA)
+}
+
+// TODO add tests for already merged PR and closed PR
diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go
new file mode 100644
index 0000000000..6ebad106fb
--- /dev/null
+++ b/tests/integration/api_pull_review_test.go
@@ -0,0 +1,307 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/json"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIPullReview(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+ assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
+
+ // test ListPullReviews
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var reviews []*api.PullReview
+ DecodeJSON(t, resp, &reviews)
+ if !assert.Len(t, reviews, 6) {
+ return
+ }
+ for _, r := range reviews {
+ assert.EqualValues(t, pullIssue.HTMLURL(), r.HTMLPullURL)
+ }
+ assert.EqualValues(t, 8, reviews[3].ID)
+ assert.EqualValues(t, "APPROVED", reviews[3].State)
+ assert.EqualValues(t, 0, reviews[3].CodeCommentsCount)
+ assert.True(t, reviews[3].Stale)
+ assert.False(t, reviews[3].Official)
+
+ assert.EqualValues(t, 10, reviews[5].ID)
+ assert.EqualValues(t, "REQUEST_CHANGES", reviews[5].State)
+ assert.EqualValues(t, 1, reviews[5].CodeCommentsCount)
+ assert.EqualValues(t, -1, reviews[5].Reviewer.ID) // ghost user
+ assert.False(t, reviews[5].Stale)
+ assert.True(t, reviews[5].Official)
+
+ // test GetPullReview
+ req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, reviews[3].ID, token)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var review api.PullReview
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, *reviews[3], review)
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, reviews[5].ID, token)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, *reviews[5], review)
+
+ // test GetPullReviewComments
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 7})
+ req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, 10, token)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var reviewComments []*api.PullReviewComment
+ DecodeJSON(t, resp, &reviewComments)
+ assert.Len(t, reviewComments, 1)
+ assert.EqualValues(t, "Ghost", reviewComments[0].Poster.UserName)
+ assert.EqualValues(t, "a review from a deleted user", reviewComments[0].Body)
+ assert.EqualValues(t, comment.ID, reviewComments[0].ID)
+ assert.EqualValues(t, comment.UpdatedUnix, reviewComments[0].Updated.Unix())
+ assert.EqualValues(t, comment.HTMLURL(), reviewComments[0].HTMLURL)
+
+ // test CreatePullReview
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ Body: "body1",
+ // Event: "" # will result in PENDING
+ Comments: []api.CreatePullReviewComment{
+ {
+ Path: "README.md",
+ Body: "first new line",
+ OldLineNum: 0,
+ NewLineNum: 1,
+ }, {
+ Path: "README.md",
+ Body: "first old line",
+ OldLineNum: 1,
+ NewLineNum: 0,
+ }, {
+ Path: "iso-8859-1.txt",
+ Body: "this line contains a non-utf-8 character",
+ OldLineNum: 0,
+ NewLineNum: 1,
+ },
+ },
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, 6, review.ID)
+ assert.EqualValues(t, "PENDING", review.State)
+ assert.EqualValues(t, 3, review.CodeCommentsCount)
+
+ // test SubmitPullReview
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token), &api.SubmitPullReviewOptions{
+ Event: "APPROVED",
+ Body: "just two nits",
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, 6, review.ID)
+ assert.EqualValues(t, "APPROVED", review.State)
+ assert.EqualValues(t, 3, review.CodeCommentsCount)
+
+ // test dismiss review
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token), &api.DismissPullReviewOptions{
+ Message: "test",
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, 6, review.ID)
+ assert.True(t, review.Dismissed)
+
+ // test dismiss review
+ req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, 6, review.ID)
+ assert.False(t, review.Dismissed)
+
+ // test DeletePullReview
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ Body: "just a comment",
+ Event: "COMMENT",
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &review)
+ assert.EqualValues(t, "COMMENT", review.State)
+ assert.EqualValues(t, 0, review.CodeCommentsCount)
+ req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token)
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ // test CreatePullReview Comment without body but with comments
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ // Body: "",
+ Event: "COMMENT",
+ Comments: []api.CreatePullReviewComment{
+ {
+ Path: "README.md",
+ Body: "first new line",
+ OldLineNum: 0,
+ NewLineNum: 1,
+ }, {
+ Path: "README.md",
+ Body: "first old line",
+ OldLineNum: 1,
+ NewLineNum: 0,
+ },
+ },
+ })
+ var commentReview api.PullReview
+
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &commentReview)
+ assert.EqualValues(t, "COMMENT", commentReview.State)
+ assert.EqualValues(t, 2, commentReview.CodeCommentsCount)
+ assert.EqualValues(t, "", commentReview.Body)
+ assert.EqualValues(t, false, commentReview.Dismissed)
+
+ // test CreatePullReview Comment with body but without comments
+ commentBody := "This is a body of the comment."
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ Body: commentBody,
+ Event: "COMMENT",
+ Comments: []api.CreatePullReviewComment{},
+ })
+
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &commentReview)
+ assert.EqualValues(t, "COMMENT", commentReview.State)
+ assert.EqualValues(t, 0, commentReview.CodeCommentsCount)
+ assert.EqualValues(t, commentBody, commentReview.Body)
+ assert.EqualValues(t, false, commentReview.Dismissed)
+
+ // test CreatePullReview Comment without body and no comments
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{
+ Body: "",
+ Event: "COMMENT",
+ Comments: []api.CreatePullReviewComment{},
+ })
+ resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ errMap := make(map[string]interface{})
+ json.Unmarshal(resp.Body.Bytes(), &errMap)
+ assert.EqualValues(t, "review event COMMENT requires a body or a comment", errMap["message"].(string))
+
+ // test get review requests
+ // to make it simple, use same api with get review
+ pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
+ assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext))
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID})
+
+ req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &reviews)
+ assert.EqualValues(t, 11, reviews[0].ID)
+ assert.EqualValues(t, "REQUEST_REVIEW", reviews[0].State)
+ assert.EqualValues(t, 0, reviews[0].CodeCommentsCount)
+ assert.False(t, reviews[0].Stale)
+ assert.True(t, reviews[0].Official)
+ assert.EqualValues(t, "test_team", reviews[0].ReviewerTeam.Name)
+
+ assert.EqualValues(t, 12, reviews[1].ID)
+ assert.EqualValues(t, "REQUEST_REVIEW", reviews[1].State)
+ assert.EqualValues(t, 0, reviews[0].CodeCommentsCount)
+ assert.False(t, reviews[1].Stale)
+ assert.True(t, reviews[1].Official)
+ assert.EqualValues(t, 1, reviews[1].Reviewer.ID)
+}
+
+func TestAPIPullReviewRequest(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+ assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext))
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
+
+ // Test add Review Request
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
+ Reviewers: []string{"user4@example.com", "user8"},
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ // poster of pr can't be reviewer
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
+ Reviewers: []string{"user1"},
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // test user not exist
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
+ Reviewers: []string{"testOther"},
+ })
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test Remove Review Request
+ session2 := loginUser(t, "user4")
+ token2 := getTokenForLoggedInUser(t, session2)
+
+ req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token2), &api.PullReviewRequestOptions{
+ Reviewers: []string{"user4"},
+ })
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ // doer is not admin
+ req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token2), &api.PullReviewRequestOptions{
+ Reviewers: []string{"user8"},
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
+ Reviewers: []string{"user8"},
+ })
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ // Test team review request
+ pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
+ assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext))
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue12.RepoID})
+
+ // Test add Team Review Request
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
+ TeamReviewers: []string{"team1", "owners"},
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ // Test add Team Review Request to not allowned
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
+ TeamReviewers: []string{"test_team"},
+ })
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // Test add Team Review Request to not exist
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
+ TeamReviewers: []string{"not_exist_team"},
+ })
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test Remove team Review Request
+ req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{
+ TeamReviewers: []string{"team1"},
+ })
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ // empty request test
+ req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{})
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo3.OwnerName, repo3.Name, pullIssue12.Index, token), &api.PullReviewRequestOptions{})
+ session.MakeRequest(t, req, http.StatusNoContent)
+}
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
new file mode 100644
index 0000000000..032912a073
--- /dev/null
+++ b/tests/integration/api_pull_test.go
@@ -0,0 +1,185 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/forms"
+ issue_service "code.gitea.io/gitea/services/issue"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIViewPulls(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all&token="+token, owner.Name, repo.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var pulls []*api.PullRequest
+ DecodeJSON(t, resp, &pulls)
+ expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true))
+ assert.Len(t, pulls, expectedLen)
+}
+
+// TestAPIMergePullWIP ensures that we can't merge a WIP pull request
+func TestAPIMergePullWIP(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{Status: issues_model.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false))
+ pr.LoadIssue()
+ issue_service.ChangeTitle(pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
+
+ // force reload
+ pr.LoadAttributes()
+
+ assert.Contains(t, pr.Issue.Title, setting.Repository.PullRequest.WorkInProgressPrefixes[0])
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", owner.Name, repo.Name, pr.Index, token), &forms.MergePullRequestForm{
+ MergeMessageField: pr.Issue.Title,
+ Do: string(repo_model.MergeStyleMerge),
+ })
+
+ session.MakeRequest(t, req, http.StatusMethodNotAllowed)
+}
+
+func TestAPICreatePullSuccess(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
+ // repo10 have code, pulls units.
+ repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
+ // repo11 only have code unit but should still create pulls
+ owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
+ owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
+
+ session := loginUser(t, owner11.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), &api.CreatePullRequestOption{
+ Head: fmt.Sprintf("%s:master", owner11.Name),
+ Base: "master",
+ Title: "create a failure pr",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
+}
+
+func TestAPICreatePullWithFieldsSuccess(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ // repo10 have code, pulls units.
+ repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
+ owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
+ // repo11 only have code unit but should still create pulls
+ repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
+ owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
+
+ session := loginUser(t, owner11.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ opts := &api.CreatePullRequestOption{
+ Head: fmt.Sprintf("%s:master", owner11.Name),
+ Base: "master",
+ Title: "create a failure pr",
+ Body: "foobaaar",
+ Milestone: 5,
+ Assignees: []string{owner10.Name},
+ Labels: []int64{5},
+ }
+
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), opts)
+
+ res := session.MakeRequest(t, req, http.StatusCreated)
+ pull := new(api.PullRequest)
+ DecodeJSON(t, res, pull)
+
+ assert.NotNil(t, pull.Milestone)
+ assert.EqualValues(t, opts.Milestone, pull.Milestone.ID)
+ if assert.Len(t, pull.Assignees, 1) {
+ assert.EqualValues(t, opts.Assignees[0], owner10.Name)
+ }
+ assert.NotNil(t, pull.Labels)
+ assert.EqualValues(t, opts.Labels[0], pull.Labels[0].ID)
+}
+
+func TestAPICreatePullWithFieldsFailure(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ // repo10 have code, pulls units.
+ repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
+ owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
+ // repo11 only have code unit but should still create pulls
+ repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
+ owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
+
+ session := loginUser(t, owner11.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ opts := &api.CreatePullRequestOption{
+ Head: fmt.Sprintf("%s:master", owner11.Name),
+ Base: "master",
+ }
+
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), opts)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ opts.Title = "is required"
+
+ opts.Milestone = 666
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ opts.Milestone = 5
+
+ opts.Assignees = []string{"qweruqweroiuyqweoiruywqer"}
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ opts.Assignees = []string{owner10.LoginName}
+
+ opts.Labels = []int64{55555}
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ opts.Labels = []int64{5}
+}
+
+func TestAPIEditPull(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
+ owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
+
+ session := loginUser(t, owner10.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), &api.CreatePullRequestOption{
+ Head: "develop",
+ Base: "master",
+ Title: "create a success pr",
+ })
+ pull := new(api.PullRequest)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, pull)
+ assert.EqualValues(t, "master", pull.Base.Name)
+
+ req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d?token=%s", owner10.Name, repo10.Name, pull.Index, token), &api.EditPullRequestOption{
+ Base: "feature/1",
+ Title: "edit a this pr",
+ })
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, pull)
+ assert.EqualValues(t, "feature/1", pull.Base.Name)
+
+ req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d?token=%s", owner10.Name, repo10.Name, pull.Index, token), &api.EditPullRequestOption{
+ Base: "not-exist",
+ })
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go
new file mode 100644
index 0000000000..0c7f5e2d52
--- /dev/null
+++ b/tests/integration/api_releases_test.go
@@ -0,0 +1,233 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIListReleases(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ token := getUserToken(t, user2.LowerName)
+
+ link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
+ link.RawQuery = url.Values{"token": {token}}.Encode()
+ resp := MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ var apiReleases []*api.Release
+ DecodeJSON(t, resp, &apiReleases)
+ if assert.Len(t, apiReleases, 3) {
+ for _, release := range apiReleases {
+ switch release.ID {
+ case 1:
+ assert.False(t, release.IsDraft)
+ assert.False(t, release.IsPrerelease)
+ case 4:
+ assert.True(t, release.IsDraft)
+ assert.False(t, release.IsPrerelease)
+ case 5:
+ assert.False(t, release.IsDraft)
+ assert.True(t, release.IsPrerelease)
+ default:
+ assert.NoError(t, fmt.Errorf("unexpected release: %v", release))
+ }
+ }
+ }
+
+ // test filter
+ testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) {
+ if auth {
+ query.Set("token", token)
+ }
+ link.RawQuery = query.Encode()
+ resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ DecodeJSON(t, resp, &apiReleases)
+ assert.Len(t, apiReleases, expectedLength, msgAndArgs)
+ }
+
+ testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts")
+ testFilterByLen(true, url.Values{"draft": {"true"}}, 1, "repo owner should see drafts")
+ testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts")
+ testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases")
+ testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release")
+ testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
+}
+
+func createNewReleaseUsingAPI(t *testing.T, session *TestSession, token string, owner *user_model.User, repo *repo_model.Repository, name, target, title, desc string) *api.Release {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases?token=%s",
+ owner.Name, repo.Name, token)
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateReleaseOption{
+ TagName: name,
+ Title: title,
+ Note: desc,
+ IsDraft: false,
+ IsPrerelease: false,
+ Target: target,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var newRelease api.Release
+ DecodeJSON(t, resp, &newRelease)
+ rel := &repo_model.Release{
+ ID: newRelease.ID,
+ TagName: newRelease.TagName,
+ Title: newRelease.Title,
+ }
+ unittest.AssertExistsAndLoadBean(t, rel)
+ assert.EqualValues(t, newRelease.Note, rel.Note)
+
+ return &newRelease
+}
+
+func TestAPICreateAndUpdateRelease(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.LowerName)
+ token := getTokenForLoggedInUser(t, session)
+
+ gitRepo, err := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ err = gitRepo.CreateTag("v0.0.1", "master")
+ assert.NoError(t, err)
+
+ target, err := gitRepo.GetTagCommitID("v0.0.1")
+ assert.NoError(t, err)
+
+ newRelease := createNewReleaseUsingAPI(t, session, token, owner, repo, "v0.0.1", target, "v0.0.1", "test")
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d?token=%s",
+ owner.Name, repo.Name, newRelease.ID, token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var release api.Release
+ DecodeJSON(t, resp, &release)
+
+ assert.Equal(t, newRelease.TagName, release.TagName)
+ assert.Equal(t, newRelease.Title, release.Title)
+ assert.Equal(t, newRelease.Note, release.Note)
+
+ req = NewRequestWithJSON(t, "PATCH", urlStr, &api.EditReleaseOption{
+ TagName: release.TagName,
+ Title: release.Title,
+ Note: "updated",
+ IsDraft: &release.IsDraft,
+ IsPrerelease: &release.IsPrerelease,
+ Target: release.Target,
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &newRelease)
+ rel := &repo_model.Release{
+ ID: newRelease.ID,
+ TagName: newRelease.TagName,
+ Title: newRelease.Title,
+ }
+ unittest.AssertExistsAndLoadBean(t, rel)
+ assert.EqualValues(t, rel.Note, newRelease.Note)
+}
+
+func TestAPICreateReleaseToDefaultBranch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.LowerName)
+ token := getTokenForLoggedInUser(t, session)
+
+ createNewReleaseUsingAPI(t, session, token, owner, repo, "v0.0.1", "", "v0.0.1", "test")
+}
+
+func TestAPICreateReleaseToDefaultBranchOnExistingTag(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.LowerName)
+ token := getTokenForLoggedInUser(t, session)
+
+ gitRepo, err := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ err = gitRepo.CreateTag("v0.0.1", "master")
+ assert.NoError(t, err)
+
+ createNewReleaseUsingAPI(t, session, token, owner, repo, "v0.0.1", "", "v0.0.1", "test")
+}
+
+func TestAPIGetReleaseByTag(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.LowerName)
+
+ tag := "v1.1"
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s",
+ owner.Name, repo.Name, tag)
+
+ req := NewRequestf(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var release *api.Release
+ DecodeJSON(t, resp, &release)
+
+ assert.Equal(t, "testing-release", release.Title)
+
+ nonexistingtag := "nonexistingtag"
+
+ urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s",
+ owner.Name, repo.Name, nonexistingtag)
+
+ req = NewRequestf(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusNotFound)
+
+ var err *api.APIError
+ DecodeJSON(t, resp, &err)
+ assert.NotEmpty(t, err.Message)
+}
+
+func TestAPIDeleteReleaseByTagName(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.LowerName)
+ token := getTokenForLoggedInUser(t, session)
+
+ createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test")
+
+ // delete release
+ req := NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/release-tag?token=%s", owner.Name, repo.Name, token))
+ _ = session.MakeRequest(t, req, http.StatusNoContent)
+
+ // make sure release is deleted
+ req = NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/release-tag?token=%s", owner.Name, repo.Name, token))
+ _ = session.MakeRequest(t, req, http.StatusNotFound)
+
+ // delete release tag too
+ req = NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/tags/release-tag?token=%s", owner.Name, repo.Name, token))
+ _ = session.MakeRequest(t, req, http.StatusNoContent)
+}
diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go
new file mode 100644
index 0000000000..3707cb7c1c
--- /dev/null
+++ b/tests/integration/api_repo_archive_test.go
@@ -0,0 +1,54 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIDownloadArchive(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user2.LowerName)
+ token := getTokenForLoggedInUser(t, session)
+
+ link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.zip", user2.Name, repo.Name))
+ link.RawQuery = url.Values{"token": {token}}.Encode()
+ resp := MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ bs, err := io.ReadAll(resp.Body)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 320, len(bs))
+
+ link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.tar.gz", user2.Name, repo.Name))
+ link.RawQuery = url.Values{"token": {token}}.Encode()
+ resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ bs, err = io.ReadAll(resp.Body)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 266, len(bs))
+
+ link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.bundle", user2.Name, repo.Name))
+ link.RawQuery = url.Values{"token": {token}}.Encode()
+ resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
+ bs, err = io.ReadAll(resp.Body)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 382, len(bs))
+
+ link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name))
+ link.RawQuery = url.Values{"token": {token}}.Encode()
+ MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusBadRequest)
+}
diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go
new file mode 100644
index 0000000000..3527e16572
--- /dev/null
+++ b/tests/integration/api_repo_collaborator_test.go
@@ -0,0 +1,131 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIRepoCollaboratorPermission(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ repo2Owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID})
+
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+ user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
+ user11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11})
+
+ session := loginUser(t, repo2Owner.Name)
+ testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name)
+
+ t.Run("RepoOwnerShouldBeOwner", func(t *testing.T) {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, repo2Owner.Name, testCtx.Token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var repoPermission api.RepoCollaboratorPermission
+ DecodeJSON(t, resp, &repoPermission)
+
+ assert.Equal(t, "owner", repoPermission.Permission)
+ })
+
+ t.Run("CollaboratorWithReadAccess", func(t *testing.T) {
+ t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeRead))
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user4.Name, testCtx.Token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var repoPermission api.RepoCollaboratorPermission
+ DecodeJSON(t, resp, &repoPermission)
+
+ assert.Equal(t, "read", repoPermission.Permission)
+ })
+
+ t.Run("CollaboratorWithWriteAccess", func(t *testing.T) {
+ t.Run("AddUserAsCollaboratorWithWriteAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeWrite))
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user4.Name, testCtx.Token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var repoPermission api.RepoCollaboratorPermission
+ DecodeJSON(t, resp, &repoPermission)
+
+ assert.Equal(t, "write", repoPermission.Permission)
+ })
+
+ t.Run("CollaboratorWithAdminAccess", func(t *testing.T) {
+ t.Run("AddUserAsCollaboratorWithAdminAccess", doAPIAddCollaborator(testCtx, user4.Name, perm.AccessModeAdmin))
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user4.Name, testCtx.Token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var repoPermission api.RepoCollaboratorPermission
+ DecodeJSON(t, resp, &repoPermission)
+
+ assert.Equal(t, "admin", repoPermission.Permission)
+ })
+
+ t.Run("CollaboratorNotFound", func(t *testing.T) {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, "non-existent-user", testCtx.Token)
+ session.MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) {
+ t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead))
+
+ _session := loginUser(t, user5.Name)
+ _testCtx := NewAPITestContext(t, user5.Name, repo2.Name)
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user5.Name, _testCtx.Token)
+ resp := _session.MakeRequest(t, req, http.StatusOK)
+
+ var repoPermission api.RepoCollaboratorPermission
+ DecodeJSON(t, resp, &repoPermission)
+
+ assert.Equal(t, "read", repoPermission.Permission)
+ })
+
+ t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) {
+ t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead))
+
+ _session := loginUser(t, user5.Name)
+ _testCtx := NewAPITestContext(t, user5.Name, repo2.Name)
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user5.Name, _testCtx.Token)
+ resp := _session.MakeRequest(t, req, http.StatusOK)
+
+ var repoPermission api.RepoCollaboratorPermission
+ DecodeJSON(t, resp, &repoPermission)
+
+ assert.Equal(t, "read", repoPermission.Permission)
+ })
+
+ t.Run("RepoAdminCanQueryACollaboratorsPermissions", func(t *testing.T) {
+ t.Run("AddUserAsCollaboratorWithAdminAccess", doAPIAddCollaborator(testCtx, user10.Name, perm.AccessModeAdmin))
+ t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user11.Name, perm.AccessModeRead))
+
+ _session := loginUser(t, user10.Name)
+ _testCtx := NewAPITestContext(t, user10.Name, repo2.Name)
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user11.Name, _testCtx.Token)
+ resp := _session.MakeRequest(t, req, http.StatusOK)
+
+ var repoPermission api.RepoCollaboratorPermission
+ DecodeJSON(t, resp, &repoPermission)
+
+ assert.Equal(t, "read", repoPermission.Permission)
+ })
+ })
+}
diff --git a/tests/integration/api_repo_edit_test.go b/tests/integration/api_repo_edit_test.go
new file mode 100644
index 0000000000..5ef92bf47c
--- /dev/null
+++ b/tests/integration/api_repo_edit_test.go
@@ -0,0 +1,346 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ unit_model "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// getRepoEditOptionFromRepo gets the options for an existing repo exactly as is
+func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption {
+ name := repo.Name
+ description := repo.Description
+ website := repo.Website
+ private := repo.IsPrivate
+ hasIssues := false
+ var internalTracker *api.InternalTracker
+ var externalTracker *api.ExternalTracker
+ if unit, err := repo.GetUnit(unit_model.TypeIssues); err == nil {
+ config := unit.IssuesConfig()
+ hasIssues = true
+ internalTracker = &api.InternalTracker{
+ EnableTimeTracker: config.EnableTimetracker,
+ AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
+ EnableIssueDependencies: config.EnableDependencies,
+ }
+ } else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil {
+ config := unit.ExternalTrackerConfig()
+ hasIssues = true
+ externalTracker = &api.ExternalTracker{
+ ExternalTrackerURL: config.ExternalTrackerURL,
+ ExternalTrackerFormat: config.ExternalTrackerFormat,
+ ExternalTrackerStyle: config.ExternalTrackerStyle,
+ }
+ }
+ hasWiki := false
+ var externalWiki *api.ExternalWiki
+ if _, err := repo.GetUnit(unit_model.TypeWiki); err == nil {
+ hasWiki = true
+ } else if unit, err := repo.GetUnit(unit_model.TypeExternalWiki); err == nil {
+ hasWiki = true
+ config := unit.ExternalWikiConfig()
+ externalWiki = &api.ExternalWiki{
+ ExternalWikiURL: config.ExternalWikiURL,
+ }
+ }
+ defaultBranch := repo.DefaultBranch
+ hasPullRequests := false
+ ignoreWhitespaceConflicts := false
+ allowMerge := false
+ allowRebase := false
+ allowRebaseMerge := false
+ allowSquash := false
+ if unit, err := repo.GetUnit(unit_model.TypePullRequests); err == nil {
+ config := unit.PullRequestsConfig()
+ hasPullRequests = true
+ ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts
+ allowMerge = config.AllowMerge
+ allowRebase = config.AllowRebase
+ allowRebaseMerge = config.AllowRebaseMerge
+ allowSquash = config.AllowSquash
+ }
+ archived := repo.IsArchived
+ return &api.EditRepoOption{
+ Name: &name,
+ Description: &description,
+ Website: &website,
+ Private: &private,
+ HasIssues: &hasIssues,
+ ExternalTracker: externalTracker,
+ InternalTracker: internalTracker,
+ HasWiki: &hasWiki,
+ ExternalWiki: externalWiki,
+ DefaultBranch: &defaultBranch,
+ HasPullRequests: &hasPullRequests,
+ IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
+ AllowMerge: &allowMerge,
+ AllowRebase: &allowRebase,
+ AllowRebaseMerge: &allowRebaseMerge,
+ AllowSquash: &allowSquash,
+ Archived: &archived,
+ }
+}
+
+// getNewRepoEditOption Gets the options to change everything about an existing repo by adding to strings or changing
+// the boolean
+func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption {
+ // Gives a new property to everything
+ name := *opts.Name + "renamed"
+ description := "new description"
+ website := "http://wwww.newwebsite.com"
+ private := !*opts.Private
+ hasIssues := !*opts.HasIssues
+ hasWiki := !*opts.HasWiki
+ defaultBranch := "master"
+ hasPullRequests := !*opts.HasPullRequests
+ ignoreWhitespaceConflicts := !*opts.IgnoreWhitespaceConflicts
+ allowMerge := !*opts.AllowMerge
+ allowRebase := !*opts.AllowRebase
+ allowRebaseMerge := !*opts.AllowRebaseMerge
+ allowSquash := !*opts.AllowSquash
+ archived := !*opts.Archived
+
+ return &api.EditRepoOption{
+ Name: &name,
+ Description: &description,
+ Website: &website,
+ Private: &private,
+ DefaultBranch: &defaultBranch,
+ HasIssues: &hasIssues,
+ HasWiki: &hasWiki,
+ HasPullRequests: &hasPullRequests,
+ IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
+ AllowMerge: &allowMerge,
+ AllowRebase: &allowRebase,
+ AllowRebaseMerge: &allowRebaseMerge,
+ AllowSquash: &allowSquash,
+ Archived: &archived,
+ }
+}
+
+func TestAPIRepoEdit(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ bFalse, bTrue := false, true
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo15 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15}) // empty repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+
+ // Test editing a repo1 which user2 owns, changing name and many properties
+ origRepoEditOption := getRepoEditOptionFromRepo(repo1)
+ repoEditOption := getNewRepoEditOption(origRepoEditOption)
+ url := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token2)
+ req := NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var repo api.Repository
+ DecodeJSON(t, resp, &repo)
+ assert.NotNil(t, repo)
+ // check response
+ assert.Equal(t, *repoEditOption.Name, repo.Name)
+ assert.Equal(t, *repoEditOption.Description, repo.Description)
+ assert.Equal(t, *repoEditOption.Website, repo.Website)
+ assert.Equal(t, *repoEditOption.Archived, repo.Archived)
+ // check repo1 from database
+ repo1edited := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ repo1editedOption := getRepoEditOptionFromRepo(repo1edited)
+ assert.Equal(t, *repoEditOption.Name, *repo1editedOption.Name)
+ assert.Equal(t, *repoEditOption.Description, *repo1editedOption.Description)
+ assert.Equal(t, *repoEditOption.Website, *repo1editedOption.Website)
+ assert.Equal(t, *repoEditOption.Archived, *repo1editedOption.Archived)
+ assert.Equal(t, *repoEditOption.Private, *repo1editedOption.Private)
+ assert.Equal(t, *repoEditOption.HasWiki, *repo1editedOption.HasWiki)
+
+ // Test editing repo1 to use internal issue and wiki (default)
+ *repoEditOption.HasIssues = true
+ repoEditOption.ExternalTracker = nil
+ repoEditOption.InternalTracker = &api.InternalTracker{
+ EnableTimeTracker: false,
+ AllowOnlyContributorsToTrackTime: false,
+ EnableIssueDependencies: false,
+ }
+ *repoEditOption.HasWiki = true
+ repoEditOption.ExternalWiki = nil
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &repo)
+ assert.NotNil(t, repo)
+ // check repo1 was written to database
+ repo1edited = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ repo1editedOption = getRepoEditOptionFromRepo(repo1edited)
+ assert.Equal(t, *repo1editedOption.HasIssues, true)
+ assert.Nil(t, repo1editedOption.ExternalTracker)
+ assert.Equal(t, *repo1editedOption.InternalTracker, *repoEditOption.InternalTracker)
+ assert.Equal(t, *repo1editedOption.HasWiki, true)
+ assert.Nil(t, repo1editedOption.ExternalWiki)
+
+ // Test editing repo1 to use external issue and wiki
+ repoEditOption.ExternalTracker = &api.ExternalTracker{
+ ExternalTrackerURL: "http://www.somewebsite.com",
+ ExternalTrackerFormat: "http://www.somewebsite.com/{user}/{repo}?issue={index}",
+ ExternalTrackerStyle: "alphanumeric",
+ }
+ repoEditOption.ExternalWiki = &api.ExternalWiki{
+ ExternalWikiURL: "http://www.somewebsite.com",
+ }
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &repo)
+ assert.NotNil(t, repo)
+ // check repo1 was written to database
+ repo1edited = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ repo1editedOption = getRepoEditOptionFromRepo(repo1edited)
+ assert.Equal(t, *repo1editedOption.HasIssues, true)
+ assert.Equal(t, *repo1editedOption.ExternalTracker, *repoEditOption.ExternalTracker)
+ assert.Equal(t, *repo1editedOption.HasWiki, true)
+ assert.Equal(t, *repo1editedOption.ExternalWiki, *repoEditOption.ExternalWiki)
+
+ // Do some tests with invalid URL for external tracker and wiki
+ repoEditOption.ExternalTracker.ExternalTrackerURL = "htp://www.somewebsite.com"
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ repoEditOption.ExternalTracker.ExternalTrackerURL = "http://www.somewebsite.com"
+ repoEditOption.ExternalTracker.ExternalTrackerFormat = "http://www.somewebsite.com/{user/{repo}?issue={index}"
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ repoEditOption.ExternalTracker.ExternalTrackerFormat = "http://www.somewebsite.com/{user}/{repo}?issue={index}"
+ repoEditOption.ExternalWiki.ExternalWikiURL = "htp://www.somewebsite.com"
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // Test small repo change through API with issue and wiki option not set; They shall not be touched.
+ *repoEditOption.Description = "small change"
+ repoEditOption.HasIssues = nil
+ repoEditOption.ExternalTracker = nil
+ repoEditOption.HasWiki = nil
+ repoEditOption.ExternalWiki = nil
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &repo)
+ assert.NotNil(t, repo)
+ // check repo1 was written to database
+ repo1edited = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ repo1editedOption = getRepoEditOptionFromRepo(repo1edited)
+ assert.Equal(t, *repo1editedOption.Description, *repoEditOption.Description)
+ assert.Equal(t, *repo1editedOption.HasIssues, true)
+ assert.NotNil(t, *repo1editedOption.ExternalTracker)
+ assert.Equal(t, *repo1editedOption.HasWiki, true)
+ assert.NotNil(t, *repo1editedOption.ExternalWiki)
+
+ // reset repo in db
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption)
+ _ = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test editing a non-existing repo
+ name := "repodoesnotexist"
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{Name: &name})
+ _ = session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test editing repo16 by user4 who does not have write access
+ origRepoEditOption = getRepoEditOptionFromRepo(repo16)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token4)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Tests a repo with no token given so will fail
+ origRepoEditOption = getRepoEditOptionFromRepo(repo16)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo16.Name)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ _ = session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using access token for a private repo that the user of the token owns
+ origRepoEditOption = getRepoEditOptionFromRepo(repo16)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ _ = session.MakeRequest(t, req, http.StatusOK)
+ // reset repo in db
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption)
+ _ = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test making a repo public that is private
+ repo16 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16})
+ assert.True(t, repo16.IsPrivate)
+ repoEditOption = &api.EditRepoOption{
+ Private: &bFalse,
+ }
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ _ = session.MakeRequest(t, req, http.StatusOK)
+ repo16 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16})
+ assert.False(t, repo16.IsPrivate)
+ // Make it private again
+ repoEditOption.Private = &bTrue
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ _ = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test to change empty repo
+ assert.False(t, repo15.IsArchived)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo15.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
+ Archived: &bTrue,
+ })
+ _ = session.MakeRequest(t, req, http.StatusOK)
+ repo15 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15})
+ assert.True(t, repo15.IsArchived)
+ req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
+ Archived: &bFalse,
+ })
+ _ = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ origRepoEditOption = getRepoEditOptionFromRepo(repo3)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, repo3.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusOK)
+ // reset repo in db
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, *repoEditOption.Name, token2)
+ req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption)
+ _ = session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" with no user token
+ origRepoEditOption = getRepoEditOptionFromRepo(repo3)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s", user3.Name, repo3.Name)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using repo "user2/repo1" where user4 is a NOT collaborator
+ origRepoEditOption = getRepoEditOptionFromRepo(repo1)
+ repoEditOption = getNewRepoEditOption(origRepoEditOption)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token4)
+ req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
+ session.MakeRequest(t, req, http.StatusForbidden)
+ })
+}
diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go
new file mode 100644
index 0000000000..f03efaa0ea
--- /dev/null
+++ b/tests/integration/api_repo_file_create_test.go
@@ -0,0 +1,308 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ stdCtx "context"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "testing"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getCreateFileOptions() api.CreateFileOptions {
+ content := "This is new text"
+ contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
+ return api.CreateFileOptions{
+ FileOptions: api.FileOptions{
+ BranchName: "master",
+ NewBranchName: "master",
+ Message: "Making this new file new/file.txt",
+ Author: api.Identity{
+ Name: "Anne Doe",
+ Email: "annedoe@example.com",
+ },
+ Committer: api.Identity{
+ Name: "John Doe",
+ Email: "johndoe@example.com",
+ },
+ Dates: api.CommitDateOptions{
+ Author: time.Unix(946684810, 0),
+ Committer: time.Unix(978307190, 0),
+ },
+ },
+ Content: contentEncoded,
+ }
+}
+
+func getExpectedFileResponseForCreate(repoFullName, commitID, treePath, latestCommitSHA string) *api.FileResponse {
+ sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
+ encoding := "base64"
+ content := "VGhpcyBpcyBuZXcgdGV4dA=="
+ selfURL := setting.AppURL + "api/v1/repos/" + repoFullName + "/contents/" + treePath + "?ref=master"
+ htmlURL := setting.AppURL + repoFullName + "/src/branch/master/" + treePath
+ gitURL := setting.AppURL + "api/v1/repos/" + repoFullName + "/git/blobs/" + sha
+ downloadURL := setting.AppURL + repoFullName + "/raw/branch/master/" + treePath
+ return &api.FileResponse{
+ Content: &api.ContentsResponse{
+ Name: filepath.Base(treePath),
+ Path: treePath,
+ SHA: sha,
+ LastCommitSHA: latestCommitSHA,
+ Size: 16,
+ Type: "file",
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
+ Links: &api.FileLinksResponse{
+ Self: &selfURL,
+ GitURL: &gitURL,
+ HTMLURL: &htmlURL,
+ },
+ },
+ Commit: &api.FileCommitResponse{
+ CommitMeta: api.CommitMeta{
+ URL: setting.AppURL + "api/v1/repos/" + repoFullName + "/git/commits/" + commitID,
+ SHA: commitID,
+ },
+ HTMLURL: setting.AppURL + repoFullName + "/commit/" + commitID,
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Anne Doe",
+ Email: "annedoe@example.com",
+ },
+ Date: "2000-01-01T00:00:10Z",
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "John Doe",
+ Email: "johndoe@example.com",
+ },
+ Date: "2000-12-31T23:59:50Z",
+ },
+ Message: "Updates README.md\n",
+ },
+ Verification: &api.PayloadCommitVerification{
+ Verified: false,
+ Reason: "gpg.error.not_signed_commit",
+ Signature: "",
+ Payload: "",
+ },
+ }
+}
+
+func BenchmarkAPICreateFileSmall(b *testing.B) {
+ onGiteaRunTB(b, func(t testing.TB, u *url.URL) {
+ b := t.(*testing.B)
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+
+ for n := 0; n < b.N; n++ {
+ treePath := fmt.Sprintf("update/file%d.txt", n)
+ createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
+ }
+ })
+}
+
+func BenchmarkAPICreateFileMedium(b *testing.B) {
+ data := make([]byte, 10*1024*1024)
+
+ onGiteaRunTB(b, func(t testing.TB, u *url.URL) {
+ b := t.(*testing.B)
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ treePath := fmt.Sprintf("update/file%d.txt", n)
+ copy(data, treePath)
+ createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath)
+ }
+ })
+}
+
+func TestAPICreateFile(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ fileID := 0
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+
+ // Test creating a file in repo1 which user2 owns, try both with branch and empty branch
+ for _, branch := range [...]string{
+ "master", // Branch
+ "", // Empty branch
+ } {
+ createFileOptions := getCreateFileOptions()
+ createFileOptions.BranchName = branch
+ fileID++
+ treePath := fmt.Sprintf("new/file%d.txt", fileID)
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ gitRepo, _ := git.OpenRepository(stdCtx.Background(), repo1.RepoPath())
+ commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName)
+ latestCommit, _ := gitRepo.GetCommitByPath(treePath)
+ expectedFileResponse := getExpectedFileResponseForCreate("user2/repo1", commitID, treePath, latestCommit.ID.String())
+ var fileResponse api.FileResponse
+ DecodeJSON(t, resp, &fileResponse)
+ assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Date, fileResponse.Commit.Author.Date)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Email, fileResponse.Commit.Committer.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Name, fileResponse.Commit.Committer.Name)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Date, fileResponse.Commit.Committer.Date)
+ gitRepo.Close()
+ }
+
+ // Test creating a file in a new branch
+ createFileOptions := getCreateFileOptions()
+ createFileOptions.BranchName = repo1.DefaultBranch
+ createFileOptions.NewBranchName = "new_branch"
+ fileID++
+ treePath := fmt.Sprintf("new/file%d.txt", fileID)
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ var fileResponse api.FileResponse
+ DecodeJSON(t, resp, &fileResponse)
+ expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
+ expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID)
+ expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
+ assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
+ assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
+ assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
+ assert.EqualValues(t, createFileOptions.Message+"\n", fileResponse.Commit.Message)
+
+ // Test creating a file without a message
+ createFileOptions = getCreateFileOptions()
+ createFileOptions.Message = ""
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, &fileResponse)
+ expectedMessage := "Add '" + treePath + "'\n"
+ assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message)
+
+ // Test trying to create a file that already exists, should fail
+ createFileOptions = getCreateFileOptions()
+ treePath = "README.md"
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ expectedAPIError := context.APIError{
+ Message: "repository file already exists [path: " + treePath + "]",
+ URL: setting.API.SwaggerURL,
+ }
+ var apiError context.APIError
+ DecodeJSON(t, resp, &apiError)
+ assert.Equal(t, expectedAPIError, apiError)
+
+ // Test creating a file in repo1 by user4 who does not have write access
+ createFileOptions = getCreateFileOptions()
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Tests a repo with no token given so will fail
+ createFileOptions = getCreateFileOptions()
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using access token for a private repo that the user of the token owns
+ createFileOptions = getCreateFileOptions()
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ createFileOptions = getCreateFileOptions()
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ // Test using org repo "user3/repo3" with no user token
+ createFileOptions = getCreateFileOptions()
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using repo "user2/repo1" where user4 is a NOT collaborator
+ createFileOptions = getCreateFileOptions()
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // Test creating a file in an empty repository
+ doAPICreateRepository(NewAPITestContext(t, "user2", "empty-repo"), true)(t)
+ createFileOptions = getCreateFileOptions()
+ fileID++
+ treePath = fmt.Sprintf("new/file%d.txt", fileID)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, "empty-repo", treePath, token2)
+ req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ emptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "empty-repo"}) // public repo
+ gitRepo, _ := git.OpenRepository(stdCtx.Background(), emptyRepo.RepoPath())
+ commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName)
+ latestCommit, _ := gitRepo.GetCommitByPath(treePath)
+ expectedFileResponse := getExpectedFileResponseForCreate("user2/empty-repo", commitID, treePath, latestCommit.ID.String())
+ DecodeJSON(t, resp, &fileResponse)
+ assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Date, fileResponse.Commit.Author.Date)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Email, fileResponse.Commit.Committer.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Name, fileResponse.Commit.Committer.Name)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Date, fileResponse.Commit.Committer.Date)
+ gitRepo.Close()
+ })
+}
diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go
new file mode 100644
index 0000000000..2c8b1e381f
--- /dev/null
+++ b/tests/integration/api_repo_file_delete_test.go
@@ -0,0 +1,170 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getDeleteFileOptions() *api.DeleteFileOptions {
+ return &api.DeleteFileOptions{
+ FileOptions: api.FileOptions{
+ BranchName: "master",
+ NewBranchName: "master",
+ Message: "Removing the file new/file.txt",
+ Author: api.Identity{
+ Name: "John Doe",
+ Email: "johndoe@example.com",
+ },
+ Committer: api.Identity{
+ Name: "Jane Doe",
+ Email: "janedoe@example.com",
+ },
+ },
+ SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
+ }
+}
+
+func TestAPIDeleteFile(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ fileID := 0
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+
+ // Test deleting a file in repo1 which user2 owns, try both with branch and empty branch
+ for _, branch := range [...]string{
+ "master", // Branch
+ "", // Empty branch
+ } {
+ fileID++
+ treePath := fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ deleteFileOptions := getDeleteFileOptions()
+ deleteFileOptions.BranchName = branch
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req := NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var fileResponse api.FileResponse
+ DecodeJSON(t, resp, &fileResponse)
+ assert.NotNil(t, fileResponse)
+ assert.Nil(t, fileResponse.Content)
+ }
+
+ // Test deleting file and making the delete in a new branch
+ fileID++
+ treePath := fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ deleteFileOptions := getDeleteFileOptions()
+ deleteFileOptions.BranchName = repo1.DefaultBranch
+ deleteFileOptions.NewBranchName = "new_branch"
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req := NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var fileResponse api.FileResponse
+ DecodeJSON(t, resp, &fileResponse)
+ assert.NotNil(t, fileResponse)
+ assert.Nil(t, fileResponse.Content)
+ assert.EqualValues(t, deleteFileOptions.Message+"\n", fileResponse.Commit.Message)
+
+ // Test deleting file without a message
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ deleteFileOptions.Message = ""
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &fileResponse)
+ expectedMessage := "Delete '" + treePath + "'\n"
+ assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message)
+
+ // Test deleting a file with the wrong SHA
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ deleteFileOptions.SHA = "badsha"
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ session.MakeRequest(t, req, http.StatusBadRequest)
+
+ // Test creating a file in repo16 by user4 who does not have write access
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo16, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Tests a repo with no token given so will fail
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo16, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using access token for a private repo that the user of the token owns
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo16, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user3, repo3, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" with no user token
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user3, repo3, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using repo "user2/repo1" where user4 is a NOT collaborator
+ fileID++
+ treePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ deleteFileOptions = getDeleteFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
+ req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
+ session.MakeRequest(t, req, http.StatusForbidden)
+ })
+}
diff --git a/tests/integration/api_repo_file_get_test.go b/tests/integration/api_repo_file_get_test.go
new file mode 100644
index 0000000000..2a7a5fa634
--- /dev/null
+++ b/tests/integration/api_repo_file_get_test.go
@@ -0,0 +1,57 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "os"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIGetRawFileOrLFS(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // Test with raw file
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/README.md")
+ resp := MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
+
+ // Test with LFS
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ httpContext := NewAPITestContext(t, "user2", "repo-lfs-test")
+ doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) {
+ u.Path = httpContext.GitPath()
+ dstPath, err := os.MkdirTemp("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ u.Path = httpContext.GitPath()
+ u.User = url.UserPassword("user2", userPassword)
+
+ t.Run("Clone", doGitClone(dstPath, u))
+
+ dstPath2, err := os.MkdirTemp("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath2)
+
+ t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
+
+ lfs, _ := lfsCommitAndPushTest(t, dstPath)
+
+ reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
+ respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
+ assert.Equal(t, littleSize, respLFS.Length)
+
+ doAPIDeleteRepository(httpContext)
+ })
+ })
+}
diff --git a/tests/integration/api_repo_file_helpers.go b/tests/integration/api_repo_file_helpers.go
new file mode 100644
index 0000000000..298bae95c0
--- /dev/null
+++ b/tests/integration/api_repo_file_helpers.go
@@ -0,0 +1,29 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FileResponse, error) {
+ opts := &files_service.UpdateRepoFileOptions{
+ OldBranch: branchName,
+ TreePath: treePath,
+ Content: content,
+ IsNewFile: true,
+ Author: nil,
+ Committer: nil,
+ }
+ return files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, user, opts)
+}
+
+func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FileResponse, error) {
+ return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file")
+}
diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go
new file mode 100644
index 0000000000..a3be67ad84
--- /dev/null
+++ b/tests/integration/api_repo_file_update_test.go
@@ -0,0 +1,278 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ stdCtx "context"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getUpdateFileOptions() *api.UpdateFileOptions {
+ content := "This is updated text"
+ contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
+ return &api.UpdateFileOptions{
+ DeleteFileOptions: api.DeleteFileOptions{
+ FileOptions: api.FileOptions{
+ BranchName: "master",
+ NewBranchName: "master",
+ Message: "My update of new/file.txt",
+ Author: api.Identity{
+ Name: "John Doe",
+ Email: "johndoe@example.com",
+ },
+ Committer: api.Identity{
+ Name: "Anne Doe",
+ Email: "annedoe@example.com",
+ },
+ },
+ SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
+ },
+ Content: contentEncoded,
+ }
+}
+
+func getExpectedFileResponseForUpdate(commitID, treePath, lastCommitSHA string) *api.FileResponse {
+ sha := "08bd14b2e2852529157324de9c226b3364e76136"
+ encoding := "base64"
+ content := "VGhpcyBpcyB1cGRhdGVkIHRleHQ="
+ selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
+ htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
+ gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+ downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
+ return &api.FileResponse{
+ Content: &api.ContentsResponse{
+ Name: filepath.Base(treePath),
+ Path: treePath,
+ SHA: sha,
+ LastCommitSHA: lastCommitSHA,
+ Type: "file",
+ Size: 20,
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
+ Links: &api.FileLinksResponse{
+ Self: &selfURL,
+ GitURL: &gitURL,
+ HTMLURL: &htmlURL,
+ },
+ },
+ Commit: &api.FileCommitResponse{
+ CommitMeta: api.CommitMeta{
+ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
+ SHA: commitID,
+ },
+ HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "John Doe",
+ Email: "johndoe@example.com",
+ },
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Anne Doe",
+ Email: "annedoe@example.com",
+ },
+ },
+ Message: "My update of README.md\n",
+ },
+ Verification: &api.PayloadCommitVerification{
+ Verified: false,
+ Reason: "gpg.error.not_signed_commit",
+ Signature: "",
+ Payload: "",
+ },
+ }
+}
+
+func TestAPIUpdateFile(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ fileID := 0
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+
+ // Test updating a file in repo1 which user2 owns, try both with branch and empty branch
+ for _, branch := range [...]string{
+ "master", // Branch
+ "", // Empty branch
+ } {
+ fileID++
+ treePath := fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ updateFileOptions := getUpdateFileOptions()
+ updateFileOptions.BranchName = branch
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req := NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ gitRepo, _ := git.OpenRepository(stdCtx.Background(), repo1.RepoPath())
+ commitID, _ := gitRepo.GetBranchCommitID(updateFileOptions.NewBranchName)
+ lasCommit, _ := gitRepo.GetCommitByPath(treePath)
+ expectedFileResponse := getExpectedFileResponseForUpdate(commitID, treePath, lasCommit.ID.String())
+ var fileResponse api.FileResponse
+ DecodeJSON(t, resp, &fileResponse)
+ assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+ gitRepo.Close()
+ }
+
+ // Test updating a file in a new branch
+ updateFileOptions := getUpdateFileOptions()
+ updateFileOptions.BranchName = repo1.DefaultBranch
+ updateFileOptions.NewBranchName = "new_branch"
+ fileID++
+ treePath := fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req := NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var fileResponse api.FileResponse
+ DecodeJSON(t, resp, &fileResponse)
+ expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136"
+ expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/update/file%d.txt", fileID)
+ expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
+ assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
+ assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
+ assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
+ assert.EqualValues(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
+
+ // Test updating a file and renaming it
+ updateFileOptions = getUpdateFileOptions()
+ updateFileOptions.BranchName = repo1.DefaultBranch
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ updateFileOptions.FromPath = treePath
+ treePath = "rename/" + treePath
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &fileResponse)
+ expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136"
+ expectedHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/master/rename/update/file%d.txt", fileID)
+ expectedDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
+ assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
+ assert.EqualValues(t, expectedHTMLURL, *fileResponse.Content.HTMLURL)
+ assert.EqualValues(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
+
+ // Test updating a file without a message
+ updateFileOptions = getUpdateFileOptions()
+ updateFileOptions.Message = ""
+ updateFileOptions.BranchName = repo1.DefaultBranch
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &fileResponse)
+ expectedMessage := "Update '" + treePath + "'\n"
+ assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message)
+
+ // Test updating a file with the wrong SHA
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ updateFileOptions = getUpdateFileOptions()
+ correctSHA := updateFileOptions.SHA
+ updateFileOptions.SHA = "badsha"
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ expectedAPIError := context.APIError{
+ Message: "sha does not match [given: " + updateFileOptions.SHA + ", expected: " + correctSHA + "]",
+ URL: setting.API.SwaggerURL,
+ }
+ var apiError context.APIError
+ DecodeJSON(t, resp, &apiError)
+ assert.Equal(t, expectedAPIError, apiError)
+
+ // Test creating a file in repo1 by user4 who does not have write access
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo16, treePath)
+ updateFileOptions = getUpdateFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Tests a repo with no token given so will fail
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo16, treePath)
+ updateFileOptions = getUpdateFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using access token for a private repo that the user of the token owns
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo16, treePath)
+ updateFileOptions = getUpdateFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user3, repo3, treePath)
+ updateFileOptions = getUpdateFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" with no user token
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user3, repo3, treePath)
+ updateFileOptions = getUpdateFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using repo "user2/repo1" where user4 is a NOT collaborator
+ fileID++
+ treePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, treePath)
+ updateFileOptions = getUpdateFileOptions()
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
+ req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
+ session.MakeRequest(t, req, http.StatusForbidden)
+ })
+}
diff --git a/tests/integration/api_repo_get_contents_list_test.go b/tests/integration/api_repo_get_contents_list_test.go
new file mode 100644
index 0000000000..4f2f5cb528
--- /dev/null
+++ b/tests/integration/api_repo_get_contents_list_test.go
@@ -0,0 +1,167 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ repo_service "code.gitea.io/gitea/services/repository"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA string) []*api.ContentsResponse {
+ treePath := "README.md"
+ sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
+ selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=" + ref
+ htmlURL := setting.AppURL + "user2/repo1/src/" + refType + "/" + ref + "/" + treePath
+ gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+ downloadURL := setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath
+ return []*api.ContentsResponse{
+ {
+ Name: filepath.Base(treePath),
+ Path: treePath,
+ SHA: sha,
+ LastCommitSHA: lastCommitSHA,
+ Type: "file",
+ Size: 30,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
+ Links: &api.FileLinksResponse{
+ Self: &selfURL,
+ GitURL: &gitURL,
+ HTMLURL: &htmlURL,
+ },
+ },
+ }
+}
+
+func TestAPIGetContentsList(t *testing.T) {
+ onGiteaRun(t, testAPIGetContentsList)
+}
+
+func testAPIGetContentsList(t *testing.T, u *url.URL) {
+ /*** SETUP ***/
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ treePath := "" // root dir
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+
+ // Make a new branch in repo1
+ newBranch := "test_branch"
+ err := repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, repo1.DefaultBranch, newBranch)
+ assert.NoError(t, err)
+ // Get the commit ID of the default branch
+ gitRepo, err := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
+ // Make a new tag in repo1
+ newTag := "test_tag"
+ err = gitRepo.CreateTag(newTag, commitID)
+ assert.NoError(t, err)
+ /*** END SETUP ***/
+
+ // ref is default ref
+ ref := repo1.DefaultBranch
+ refType := "branch"
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var contentsListResponse []*api.ContentsResponse
+ DecodeJSON(t, resp, &contentsListResponse)
+ assert.NotNil(t, contentsListResponse)
+ lastCommit, err := gitRepo.GetCommitByPath("README.md")
+ assert.NoError(t, err)
+ expectedContentsListResponse := getExpectedContentsListResponseForContents(ref, refType, lastCommit.ID.String())
+ assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+ // No ref
+ refType = "branch"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsListResponse)
+ assert.NotNil(t, contentsListResponse)
+
+ expectedContentsListResponse = getExpectedContentsListResponseForContents(repo1.DefaultBranch, refType, lastCommit.ID.String())
+ assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+ // ref is the branch we created above in setup
+ ref = newBranch
+ refType = "branch"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsListResponse)
+ assert.NotNil(t, contentsListResponse)
+ branchCommit, err := gitRepo.GetBranchCommit(ref)
+ assert.NoError(t, err)
+ lastCommit, err = branchCommit.GetCommitByPath("README.md")
+ assert.NoError(t, err)
+ expectedContentsListResponse = getExpectedContentsListResponseForContents(ref, refType, lastCommit.ID.String())
+ assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+ // ref is the new tag we created above in setup
+ ref = newTag
+ refType = "tag"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsListResponse)
+ assert.NotNil(t, contentsListResponse)
+ tagCommit, err := gitRepo.GetTagCommit(ref)
+ assert.NoError(t, err)
+ lastCommit, err = tagCommit.GetCommitByPath("README.md")
+ assert.NoError(t, err)
+ expectedContentsListResponse = getExpectedContentsListResponseForContents(ref, refType, lastCommit.ID.String())
+ assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+ // ref is a commit
+ ref = commitID
+ refType = "commit"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsListResponse)
+ assert.NotNil(t, contentsListResponse)
+ expectedContentsListResponse = getExpectedContentsListResponseForContents(ref, refType, commitID)
+ assert.EqualValues(t, expectedContentsListResponse, contentsListResponse)
+
+ // Test file contents a file with a bad ref
+ ref = "badref"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test accessing private ref with user token that does not have access - should fail
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test access private ref of owner of token
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test access of org user3 private repo file by owner user2
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
+ session.MakeRequest(t, req, http.StatusOK)
+}
diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go
new file mode 100644
index 0000000000..dddc316e1a
--- /dev/null
+++ b/tests/integration/api_repo_get_contents_test.go
@@ -0,0 +1,163 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ repo_service "code.gitea.io/gitea/services/repository"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) *api.ContentsResponse {
+ treePath := "README.md"
+ sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
+ encoding := "base64"
+ content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
+ selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=" + ref
+ htmlURL := setting.AppURL + "user2/repo1/src/" + refType + "/" + ref + "/" + treePath
+ gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
+ downloadURL := setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath
+ return &api.ContentsResponse{
+ Name: treePath,
+ Path: treePath,
+ SHA: sha,
+ LastCommitSHA: lastCommitSHA,
+ Type: "file",
+ Size: 30,
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
+ Links: &api.FileLinksResponse{
+ Self: &selfURL,
+ GitURL: &gitURL,
+ HTMLURL: &htmlURL,
+ },
+ }
+}
+
+func TestAPIGetContents(t *testing.T) {
+ onGiteaRun(t, testAPIGetContents)
+}
+
+func testAPIGetContents(t *testing.T, u *url.URL) {
+ /*** SETUP ***/
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ treePath := "README.md"
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t)
+
+ // Make a new branch in repo1
+ newBranch := "test_branch"
+ err := repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, repo1.DefaultBranch, newBranch)
+ assert.NoError(t, err)
+ // Get the commit ID of the default branch
+ gitRepo, err := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
+ assert.NoError(t, err)
+ // Make a new tag in repo1
+ newTag := "test_tag"
+ err = gitRepo.CreateTag(newTag, commitID)
+ assert.NoError(t, err)
+ /*** END SETUP ***/
+
+ // ref is default ref
+ ref := repo1.DefaultBranch
+ refType := "branch"
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var contentsResponse api.ContentsResponse
+ DecodeJSON(t, resp, &contentsResponse)
+ assert.NotNil(t, contentsResponse)
+ lastCommit, _ := gitRepo.GetCommitByPath("README.md")
+ expectedContentsResponse := getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
+ assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+ // No ref
+ refType = "branch"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsResponse)
+ assert.NotNil(t, contentsResponse)
+ expectedContentsResponse = getExpectedContentsResponseForContents(repo1.DefaultBranch, refType, lastCommit.ID.String())
+ assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+ // ref is the branch we created above in setup
+ ref = newBranch
+ refType = "branch"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsResponse)
+ assert.NotNil(t, contentsResponse)
+ branchCommit, _ := gitRepo.GetBranchCommit(ref)
+ lastCommit, _ = branchCommit.GetCommitByPath("README.md")
+ expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
+ assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+ // ref is the new tag we created above in setup
+ ref = newTag
+ refType = "tag"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsResponse)
+ assert.NotNil(t, contentsResponse)
+ tagCommit, _ := gitRepo.GetTagCommit(ref)
+ lastCommit, _ = tagCommit.GetCommitByPath("README.md")
+ expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String())
+ assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+ // ref is a commit
+ ref = commitID
+ refType = "commit"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &contentsResponse)
+ assert.NotNil(t, contentsResponse)
+ expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, commitID)
+ assert.EqualValues(t, *expectedContentsResponse, contentsResponse)
+
+ // Test file contents a file with a bad ref
+ ref = "badref"
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test accessing private ref with user token that does not have access - should fail
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test access private ref of owner of token
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test access of org user3 private repo file by owner user2
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
+ session.MakeRequest(t, req, http.StatusOK)
+}
diff --git a/tests/integration/api_repo_git_blobs_test.go b/tests/integration/api_repo_git_blobs_test.go
new file mode 100644
index 0000000000..cb5116c743
--- /dev/null
+++ b/tests/integration/api_repo_git_blobs_test.go
@@ -0,0 +1,79 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIReposGitBlobs(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ repo1ReadmeSHA := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
+ repo3ReadmeSHA := "d56a3073c1dbb7b15963110a049d50cdb5db99fc"
+ repo16ReadmeSHA := "f90451c72ef61a7645293d17b47be7a8e983da57"
+ badSHA := "0000000000000000000000000000000000000000"
+
+ // Login as User2.
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t) // don't want anyone logged in for this
+
+ // Test a public repo that anyone can GET the blob of
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, repo1ReadmeSHA)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var gitBlobResponse api.GitBlobResponse
+ DecodeJSON(t, resp, &gitBlobResponse)
+ assert.NotNil(t, gitBlobResponse)
+ expectedContent := "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"
+ assert.Equal(t, expectedContent, gitBlobResponse.Content)
+
+ // Tests a private repo with no token so will fail
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo16.Name, repo16ReadmeSHA)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using access token for a private repo that the user of the token owns
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user2.Name, repo16.Name, repo16ReadmeSHA, token)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using bad sha
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, badSHA)
+ session.MakeRequest(t, req, http.StatusBadRequest)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user3.Name, repo3.Name, repo3ReadmeSHA, token)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user3.Name, repo3.Name, repo3ReadmeSHA, token)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" with no user token
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user3.Name, repo3ReadmeSHA, repo3.Name)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Login as User4.
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t) // don't want anyone logged in for this
+
+ // Test using org repo "user3/repo3" where user4 is a NOT collaborator
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", user3.Name, repo3.Name, token4)
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/api_repo_git_commits_test.go b/tests/integration/api_repo_git_commits_test.go
new file mode 100644
index 0000000000..99f83f943c
--- /dev/null
+++ b/tests/integration/api_repo_git_commits_test.go
@@ -0,0 +1,152 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func compareCommitFiles(t *testing.T, expect []string, files []*api.CommitAffectedFiles) {
+ var actual []string
+ for i := range files {
+ actual = append(actual, files[i].Filename)
+ }
+ assert.ElementsMatch(t, expect, actual)
+}
+
+func TestAPIReposGitCommits(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // check invalid requests
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/12345?token="+token, user.Name)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/..?token="+token, user.Name)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/branch-not-exist?token="+token, user.Name)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ for _, ref := range [...]string{
+ "master", // Branch
+ "v1.1", // Tag
+ "65f1", // short sha
+ "65f1bf27bc3bf70f64657658635e66094edbcb4d", // full sha
+ } {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/%s?token="+token, user.Name, ref)
+ session.MakeRequest(t, req, http.StatusOK)
+ }
+}
+
+func TestAPIReposGitCommitList(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Test getting commits (Page 1)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token, user.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiData []api.Commit
+ DecodeJSON(t, resp, &apiData)
+
+ assert.Len(t, apiData, 3)
+ assert.EqualValues(t, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", apiData[0].CommitMeta.SHA)
+ compareCommitFiles(t, []string{"readme.md"}, apiData[0].Files)
+ assert.EqualValues(t, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", apiData[1].CommitMeta.SHA)
+ compareCommitFiles(t, []string{"readme.md"}, apiData[1].Files)
+ assert.EqualValues(t, "5099b81332712fe655e34e8dd63574f503f61811", apiData[2].CommitMeta.SHA)
+ compareCommitFiles(t, []string{"readme.md"}, apiData[2].Files)
+}
+
+func TestAPIReposGitCommitListPage2Empty(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Test getting commits (Page=2)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&page=2", user.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiData []api.Commit
+ DecodeJSON(t, resp, &apiData)
+
+ assert.Len(t, apiData, 0)
+}
+
+func TestAPIReposGitCommitListDifferentBranch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Test getting commits (Page=1, Branch=good-sign)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&sha=good-sign", user.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiData []api.Commit
+ DecodeJSON(t, resp, &apiData)
+
+ assert.Len(t, apiData, 1)
+ assert.Equal(t, "f27c2b2b03dcab38beaf89b0ab4ff61f6de63441", apiData[0].CommitMeta.SHA)
+ compareCommitFiles(t, []string{"readme.md"}, apiData[0].Files)
+}
+
+func TestDownloadCommitDiffOrPatch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Test getting diff
+ reqDiff := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/git/commits/f27c2b2b03dcab38beaf89b0ab4ff61f6de63441.diff?token="+token, user.Name)
+ resp := session.MakeRequest(t, reqDiff, http.StatusOK)
+ assert.EqualValues(t,
+ "commit f27c2b2b03dcab38beaf89b0ab4ff61f6de63441\nAuthor: User2 <user2@example.com>\nDate: Sun Aug 6 19:55:01 2017 +0200\n\n good signed commit\n\ndiff --git a/readme.md b/readme.md\nnew file mode 100644\nindex 0000000..458121c\n--- /dev/null\n+++ b/readme.md\n@@ -0,0 +1 @@\n+good sign\n",
+ resp.Body.String())
+
+ // Test getting patch
+ reqPatch := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/git/commits/f27c2b2b03dcab38beaf89b0ab4ff61f6de63441.patch?token="+token, user.Name)
+ resp = session.MakeRequest(t, reqPatch, http.StatusOK)
+ assert.EqualValues(t,
+ "From f27c2b2b03dcab38beaf89b0ab4ff61f6de63441 Mon Sep 17 00:00:00 2001\nFrom: User2 <user2@example.com>\nDate: Sun, 6 Aug 2017 19:55:01 +0200\nSubject: [PATCH] good signed commit\n\n---\n readme.md | 1 +\n 1 file changed, 1 insertion(+)\n create mode 100644 readme.md\n\ndiff --git a/readme.md b/readme.md\nnew file mode 100644\nindex 0000000..458121c\n--- /dev/null\n+++ b/readme.md\n@@ -0,0 +1 @@\n+good sign\n",
+ resp.Body.String())
+}
+
+func TestGetFileHistory(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?path=readme.md&token="+token+"&sha=good-sign", user.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiData []api.Commit
+ DecodeJSON(t, resp, &apiData)
+
+ assert.Len(t, apiData, 1)
+ assert.Equal(t, "f27c2b2b03dcab38beaf89b0ab4ff61f6de63441", apiData[0].CommitMeta.SHA)
+ compareCommitFiles(t, []string{"readme.md"}, apiData[0].Files)
+}
diff --git a/tests/integration/api_repo_git_hook_test.go b/tests/integration/api_repo_git_hook_test.go
new file mode 100644
index 0000000000..a6c4f91d4a
--- /dev/null
+++ b/tests/integration/api_repo_git_hook_test.go
@@ -0,0 +1,197 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const testHookContent = `#!/bin/bash
+
+echo Hello, World!
+`
+
+func TestAPIListGitHooks(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ // user1 is an admin user
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git?token=%s",
+ owner.Name, repo.Name, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ var apiGitHooks []*api.GitHook
+ DecodeJSON(t, resp, &apiGitHooks)
+ assert.Len(t, apiGitHooks, 3)
+ for _, apiGitHook := range apiGitHooks {
+ if apiGitHook.Name == "pre-receive" {
+ assert.True(t, apiGitHook.IsActive)
+ assert.Equal(t, testHookContent, apiGitHook.Content)
+ } else {
+ assert.False(t, apiGitHook.IsActive)
+ assert.Empty(t, apiGitHook.Content)
+ }
+ }
+}
+
+func TestAPIListGitHooksNoHooks(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ // user1 is an admin user
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git?token=%s",
+ owner.Name, repo.Name, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ var apiGitHooks []*api.GitHook
+ DecodeJSON(t, resp, &apiGitHooks)
+ assert.Len(t, apiGitHooks, 3)
+ for _, apiGitHook := range apiGitHooks {
+ assert.False(t, apiGitHook.IsActive)
+ assert.Empty(t, apiGitHook.Content)
+ }
+}
+
+func TestAPIListGitHooksNoAccess(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git?token=%s",
+ owner.Name, repo.Name, token)
+ MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAPIGetGitHook(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ // user1 is an admin user
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ var apiGitHook *api.GitHook
+ DecodeJSON(t, resp, &apiGitHook)
+ assert.True(t, apiGitHook.IsActive)
+ assert.Equal(t, testHookContent, apiGitHook.Content)
+}
+
+func TestAPIGetGitHookNoAccess(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAPIEditGitHook(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ // user1 is an admin user
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{
+ Content: testHookContent,
+ })
+ resp := MakeRequest(t, req, http.StatusOK)
+ var apiGitHook *api.GitHook
+ DecodeJSON(t, resp, &apiGitHook)
+ assert.True(t, apiGitHook.IsActive)
+ assert.Equal(t, testHookContent, apiGitHook.Content)
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ var apiGitHook2 *api.GitHook
+ DecodeJSON(t, resp, &apiGitHook2)
+ assert.True(t, apiGitHook2.IsActive)
+ assert.Equal(t, testHookContent, apiGitHook2.Content)
+}
+
+func TestAPIEditGitHookNoAccess(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{
+ Content: testHookContent,
+ })
+ MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAPIDeleteGitHook(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ // user1 is an admin user
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ var apiGitHook2 *api.GitHook
+ DecodeJSON(t, resp, &apiGitHook2)
+ assert.False(t, apiGitHook2.IsActive)
+ assert.Empty(t, apiGitHook2.Content)
+}
+
+func TestAPIDeleteGitHookNoAccess(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
+ owner.Name, repo.Name, token)
+ MakeRequest(t, req, http.StatusForbidden)
+}
diff --git a/tests/integration/api_repo_git_notes_test.go b/tests/integration/api_repo_git_notes_test.go
new file mode 100644
index 0000000000..713c7599c3
--- /dev/null
+++ b/tests/integration/api_repo_git_notes_test.go
@@ -0,0 +1,41 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIReposGitNotes(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // check invalid requests
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/notes/12345?token=%s", user.Name, token)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/notes/..?token=%s", user.Name, token)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // check valid request
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/notes/65f1bf27bc3bf70f64657658635e66094edbcb4d?token=%s", user.Name, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiData api.Note
+ DecodeJSON(t, resp, &apiData)
+ assert.Equal(t, "This is a test note\n", apiData.Message)
+ })
+}
diff --git a/tests/integration/api_repo_git_ref_test.go b/tests/integration/api_repo_git_ref_test.go
new file mode 100644
index 0000000000..e8fc47f8dc
--- /dev/null
+++ b/tests/integration/api_repo_git_ref_test.go
@@ -0,0 +1,36 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestAPIReposGitRefs(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ for _, ref := range [...]string{
+ "refs/heads/master", // Branch
+ "refs/tags/v1.1", // Tag
+ } {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/%s?token="+token, user.Name, ref)
+ session.MakeRequest(t, req, http.StatusOK)
+ }
+ // Test getting all refs
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/refs?token="+token, user.Name)
+ session.MakeRequest(t, req, http.StatusOK)
+ // Test getting non-existent refs
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/refs/heads/unknown?token="+token, user.Name)
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/api_repo_git_tags_test.go b/tests/integration/api_repo_git_tags_test.go
new file mode 100644
index 0000000000..855eb2451e
--- /dev/null
+++ b/tests/integration/api_repo_git_tags_test.go
@@ -0,0 +1,88 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIGitTags(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Set up git config for the tagger
+ _ = git.NewCommand(git.DefaultContext, "config", "user.name", user.Name).Run(&git.RunOpts{Dir: repo.RepoPath()})
+ _ = git.NewCommand(git.DefaultContext, "config", "user.email", user.Email).Run(&git.RunOpts{Dir: repo.RepoPath()})
+
+ gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ defer gitRepo.Close()
+
+ commit, _ := gitRepo.GetBranchCommit("master")
+ lTagName := "lightweightTag"
+ gitRepo.CreateTag(lTagName, commit.ID.String())
+
+ aTagName := "annotatedTag"
+ aTagMessage := "my annotated message"
+ gitRepo.CreateAnnotatedTag(aTagName, aTagMessage, commit.ID.String())
+ aTag, _ := gitRepo.GetTag(aTagName)
+
+ // SHOULD work for annotated tags
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s?token=%s", user.Name, repo.Name, aTag.ID.String(), token)
+ res := session.MakeRequest(t, req, http.StatusOK)
+
+ var tag *api.AnnotatedTag
+ DecodeJSON(t, res, &tag)
+
+ assert.Equal(t, aTagName, tag.Tag)
+ assert.Equal(t, aTag.ID.String(), tag.SHA)
+ assert.Equal(t, commit.ID.String(), tag.Object.SHA)
+ assert.Equal(t, aTagMessage+"\n", tag.Message)
+ assert.Equal(t, user.Name, tag.Tagger.Name)
+ assert.Equal(t, user.Email, tag.Tagger.Email)
+ assert.Equal(t, util.URLJoin(repo.APIURL(), "git/tags", aTag.ID.String()), tag.URL)
+
+ // Should NOT work for lightweight tags
+ badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s?token=%s", user.Name, repo.Name, commit.ID.String(), token)
+ session.MakeRequest(t, badReq, http.StatusBadRequest)
+}
+
+func TestAPIDeleteTagByName(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.LowerName)
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags/delete-tag?token=%s",
+ owner.Name, repo.Name, token)
+
+ req := NewRequestf(t, http.MethodDelete, urlStr)
+ _ = session.MakeRequest(t, req, http.StatusNoContent)
+
+ // Make sure that actual releases can't be deleted outright
+ createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test")
+ urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/tags/release-tag?token=%s",
+ owner.Name, repo.Name, token)
+
+ req = NewRequestf(t, http.MethodDelete, urlStr)
+ _ = session.MakeRequest(t, req, http.StatusConflict)
+}
diff --git a/tests/integration/api_repo_git_trees_test.go b/tests/integration/api_repo_git_trees_test.go
new file mode 100644
index 0000000000..385fec12ba
--- /dev/null
+++ b/tests/integration/api_repo_git_trees_test.go
@@ -0,0 +1,77 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestAPIReposGitTrees(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ repo1TreeSHA := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
+ repo3TreeSHA := "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6"
+ repo16TreeSHA := "69554a64c1e6030f051e5c3f94bfbd773cd6a324"
+ badSHA := "0000000000000000000000000000000000000000"
+
+ // Login as User2.
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t) // don't want anyone logged in for this
+
+ // Test a public repo that anyone can GET the tree of
+ for _, ref := range [...]string{
+ "master", // Branch
+ repo1TreeSHA, // Tree SHA
+ } {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, ref)
+ session.MakeRequest(t, req, http.StatusOK)
+ }
+
+ // Tests a private repo with no token so will fail
+ for _, ref := range [...]string{
+ "master", // Branch
+ repo1TreeSHA, // Tag
+ } {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo16.Name, ref)
+ session.MakeRequest(t, req, http.StatusNotFound)
+ }
+
+ // Test using access token for a private repo that the user of the token owns
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s?token=%s", user2.Name, repo16.Name, repo16TreeSHA, token)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using bad sha
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, badSHA)
+ session.MakeRequest(t, req, http.StatusBadRequest)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s?token=%s", user3.Name, repo3.Name, repo3TreeSHA, token)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test using org repo "user3/repo3" with no user token
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user3.Name, repo3TreeSHA, repo3.Name)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Login as User4.
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session)
+ session = emptyTestSession(t) // don't want anyone logged in for this
+
+ // Test using org repo "user3/repo3" where user4 is a NOT collaborator
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", user3.Name, repo3.Name, token4)
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/api_repo_languages_test.go b/tests/integration/api_repo_languages_test.go
new file mode 100644
index 0000000000..98373fb6b1
--- /dev/null
+++ b/tests/integration/api_repo_languages_test.go
@@ -0,0 +1,50 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepoLanguages(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user2")
+
+ // Request editor page
+ req := NewRequest(t, "GET", "/user2/repo1/_new/master/")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ lastCommit := doc.GetInputValueByName("last_commit")
+ assert.NotEmpty(t, lastCommit)
+
+ // Save new file to master branch
+ req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
+ "_csrf": doc.GetCSRF(),
+ "last_commit": lastCommit,
+ "tree_path": "test.go",
+ "content": "package main",
+ "commit_choice": "direct",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // let gitea calculate language stats
+ time.Sleep(time.Second)
+
+ // Save new file to master branch
+ req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/languages")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ var languages map[string]int64
+ DecodeJSON(t, resp, &languages)
+
+ assert.InDeltaMapValues(t, map[string]int64{"Go": 12}, languages, 0)
+ })
+}
diff --git a/tests/integration/api_repo_lfs_locks_test.go b/tests/integration/api_repo_lfs_locks_test.go
new file mode 100644
index 0000000000..0860f47533
--- /dev/null
+++ b/tests/integration/api_repo_lfs_locks_test.go
@@ -0,0 +1,181 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPILFSLocksNotStarted(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ setting.LFS.StartServer = false
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ req := NewRequestf(t, "GET", "/%s/%s.git/info/lfs/locks", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "POST", "/%s/%s.git/info/lfs/locks", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "GET", "/%s/%s.git/info/lfs/locks/verify", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "GET", "/%s/%s.git/info/lfs/locks/10/unlock", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestAPILFSLocksNotLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ setting.LFS.StartServer = true
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ req := NewRequestf(t, "GET", "/%s/%s.git/info/lfs/locks", user.Name, repo.Name)
+ req.Header.Set("Accept", lfs.MediaType)
+ resp := MakeRequest(t, req, http.StatusUnauthorized)
+ var lfsLockError api.LFSLockError
+ DecodeJSON(t, resp, &lfsLockError)
+ assert.Equal(t, "You must have pull access to list locks", lfsLockError.Message)
+}
+
+func TestAPILFSLocksLogged(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ setting.LFS.StartServer = true
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // in org 3
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // in org 3
+
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // own by org 3
+
+ tests := []struct {
+ user *user_model.User
+ repo *repo_model.Repository
+ path string
+ httpResult int
+ addTime []int
+ }{
+ {user: user2, repo: repo1, path: "foo/bar.zip", httpResult: http.StatusCreated, addTime: []int{0}},
+ {user: user2, repo: repo1, path: "path/test", httpResult: http.StatusCreated, addTime: []int{0}},
+ {user: user2, repo: repo1, path: "path/test", httpResult: http.StatusConflict},
+ {user: user2, repo: repo1, path: "Foo/BaR.zip", httpResult: http.StatusConflict},
+ {user: user2, repo: repo1, path: "Foo/Test/../subFOlder/../Relative/../BaR.zip", httpResult: http.StatusConflict},
+ {user: user4, repo: repo1, path: "FoO/BaR.zip", httpResult: http.StatusUnauthorized},
+ {user: user4, repo: repo1, path: "path/test-user4", httpResult: http.StatusUnauthorized},
+ {user: user2, repo: repo1, path: "patH/Test-user4", httpResult: http.StatusCreated, addTime: []int{0}},
+ {user: user2, repo: repo1, path: "some/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/path", httpResult: http.StatusCreated, addTime: []int{0}},
+
+ {user: user2, repo: repo3, path: "test/foo/bar.zip", httpResult: http.StatusCreated, addTime: []int{1, 2}},
+ {user: user4, repo: repo3, path: "test/foo/bar.zip", httpResult: http.StatusConflict},
+ {user: user4, repo: repo3, path: "test/foo/bar.bin", httpResult: http.StatusCreated, addTime: []int{1, 2}},
+ }
+
+ resultsTests := []struct {
+ user *user_model.User
+ repo *repo_model.Repository
+ totalCount int
+ oursCount int
+ theirsCount int
+ locksOwners []*user_model.User
+ locksTimes []time.Time
+ }{
+ {user: user2, repo: repo1, totalCount: 4, oursCount: 4, theirsCount: 0, locksOwners: []*user_model.User{user2, user2, user2, user2}, locksTimes: []time.Time{}},
+ {user: user2, repo: repo3, totalCount: 2, oursCount: 1, theirsCount: 1, locksOwners: []*user_model.User{user2, user4}, locksTimes: []time.Time{}},
+ {user: user4, repo: repo3, totalCount: 2, oursCount: 1, theirsCount: 1, locksOwners: []*user_model.User{user2, user4}, locksTimes: []time.Time{}},
+ }
+
+ deleteTests := []struct {
+ user *user_model.User
+ repo *repo_model.Repository
+ lockID string
+ }{}
+
+ // create locks
+ for _, test := range tests {
+ session := loginUser(t, test.user.Name)
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks", test.repo.FullName()), map[string]string{"path": test.path})
+ req.Header.Set("Accept", lfs.MediaType)
+ req.Header.Set("Content-Type", lfs.MediaType)
+ resp := session.MakeRequest(t, req, test.httpResult)
+ if len(test.addTime) > 0 {
+ var lfsLock api.LFSLockResponse
+ DecodeJSON(t, resp, &lfsLock)
+ assert.EqualValues(t, lfsLock.Lock.LockedAt.Format(time.RFC3339), lfsLock.Lock.LockedAt.Format(time.RFC3339Nano)) // locked at should be rounded to second
+ for _, id := range test.addTime {
+ resultsTests[id].locksTimes = append(resultsTests[id].locksTimes, time.Now())
+ }
+ }
+ }
+
+ // check creation
+ for _, test := range resultsTests {
+ session := loginUser(t, test.user.Name)
+ req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
+ req.Header.Set("Accept", lfs.MediaType)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var lfsLocks api.LFSLockList
+ DecodeJSON(t, resp, &lfsLocks)
+ assert.Len(t, lfsLocks.Locks, test.totalCount)
+ for i, lock := range lfsLocks.Locks {
+ assert.EqualValues(t, test.locksOwners[i].DisplayName(), lock.Owner.Name)
+ assert.WithinDuration(t, test.locksTimes[i], lock.LockedAt, 10*time.Second)
+ assert.EqualValues(t, lock.LockedAt.Format(time.RFC3339), lock.LockedAt.Format(time.RFC3339Nano)) // locked at should be rounded to second
+ }
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/verify", test.repo.FullName()), map[string]string{})
+ req.Header.Set("Accept", lfs.MediaType)
+ req.Header.Set("Content-Type", lfs.MediaType)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var lfsLocksVerify api.LFSLockListVerify
+ DecodeJSON(t, resp, &lfsLocksVerify)
+ assert.Len(t, lfsLocksVerify.Ours, test.oursCount)
+ assert.Len(t, lfsLocksVerify.Theirs, test.theirsCount)
+ for _, lock := range lfsLocksVerify.Ours {
+ assert.EqualValues(t, test.user.DisplayName(), lock.Owner.Name)
+ deleteTests = append(deleteTests, struct {
+ user *user_model.User
+ repo *repo_model.Repository
+ lockID string
+ }{test.user, test.repo, lock.ID})
+ }
+ for _, lock := range lfsLocksVerify.Theirs {
+ assert.NotEqual(t, test.user.DisplayName(), lock.Owner.Name)
+ }
+ }
+
+ // remove all locks
+ for _, test := range deleteTests {
+ session := loginUser(t, test.user.Name)
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/%s/unlock", test.repo.FullName(), test.lockID), map[string]string{})
+ req.Header.Set("Accept", lfs.MediaType)
+ req.Header.Set("Content-Type", lfs.MediaType)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var lfsLockRep api.LFSLockResponse
+ DecodeJSON(t, resp, &lfsLockRep)
+ assert.Equal(t, test.lockID, lfsLockRep.Lock.ID)
+ assert.Equal(t, test.user.DisplayName(), lfsLockRep.Lock.Owner.Name)
+ }
+
+ // check that we don't have any lock
+ for _, test := range resultsTests {
+ session := loginUser(t, test.user.Name)
+ req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
+ req.Header.Set("Accept", lfs.MediaType)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var lfsLocks api.LFSLockList
+ DecodeJSON(t, resp, &lfsLocks)
+ assert.Len(t, lfsLocks.Locks, 0)
+ }
+}
diff --git a/tests/integration/api_repo_lfs_migrate_test.go b/tests/integration/api_repo_lfs_migrate_test.go
new file mode 100644
index 0000000000..d2edf67e8b
--- /dev/null
+++ b/tests/integration/api_repo_lfs_migrate_test.go
@@ -0,0 +1,54 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "path"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/migrations"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIRepoLFSMigrateLocal(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ oldImportLocalPaths := setting.ImportLocalPaths
+ oldAllowLocalNetworks := setting.Migrations.AllowLocalNetworks
+ setting.ImportLocalPaths = true
+ setting.Migrations.AllowLocalNetworks = true
+ assert.NoError(t, migrations.Init())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate?token="+token, &api.MigrateRepoOptions{
+ CloneAddr: path.Join(setting.RepoRootPath, "migration/lfs-test.git"),
+ RepoOwnerID: user.ID,
+ RepoName: "lfs-test-local",
+ LFS: true,
+ })
+ resp := MakeRequest(t, req, NoExpectedStatus)
+ assert.EqualValues(t, http.StatusCreated, resp.Code)
+
+ store := lfs.NewContentStore()
+ ok, _ := store.Verify(lfs.Pointer{Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041", Size: 6})
+ assert.True(t, ok)
+ ok, _ = store.Verify(lfs.Pointer{Oid: "d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0152", Size: 6})
+ assert.True(t, ok)
+
+ setting.ImportLocalPaths = oldImportLocalPaths
+ setting.Migrations.AllowLocalNetworks = oldAllowLocalNetworks
+ assert.NoError(t, migrations.Init()) // reset old migration settings
+}
diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go
new file mode 100644
index 0000000000..440dd04a81
--- /dev/null
+++ b/tests/integration/api_repo_lfs_test.go
@@ -0,0 +1,487 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "net/http"
+ "path"
+ "strconv"
+ "strings"
+ "testing"
+
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPILFSNotStarted(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.LFS.StartServer = false
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ req := NewRequestf(t, "POST", "/%s/%s.git/info/lfs/objects/batch", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "PUT", "/%s/%s.git/info/lfs/objects/oid/10", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "GET", "/%s/%s.git/info/lfs/objects/oid/name", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "GET", "/%s/%s.git/info/lfs/objects/oid", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "POST", "/%s/%s.git/info/lfs/verify", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestAPILFSMediaType(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.LFS.StartServer = true
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ req := NewRequestf(t, "POST", "/%s/%s.git/info/lfs/objects/batch", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusUnsupportedMediaType)
+ req = NewRequestf(t, "POST", "/%s/%s.git/info/lfs/verify", user.Name, repo.Name)
+ MakeRequest(t, req, http.StatusUnsupportedMediaType)
+}
+
+func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository {
+ ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo")
+ t.Run("CreateRepo", doAPICreateRepository(ctx, false))
+
+ repo, err := repo_model.GetRepositoryByOwnerAndName("user2", "lfs-"+name+"-repo")
+ assert.NoError(t, err)
+
+ return repo
+}
+
+func TestAPILFSBatch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.LFS.StartServer = true
+
+ repo := createLFSTestRepository(t, "batch")
+
+ content := []byte("dummy1")
+ oid := storeObjectInRepo(t, repo.ID, &content)
+ defer git_model.RemoveLFSMetaObjectByOid(repo.ID, oid)
+
+ session := loginUser(t, "user2")
+
+ newRequest := func(t testing.TB, br *lfs.BatchRequest) *http.Request {
+ req := NewRequestWithJSON(t, "POST", "/user2/lfs-batch-repo.git/info/lfs/objects/batch", br)
+ req.Header.Set("Accept", lfs.MediaType)
+ req.Header.Set("Content-Type", lfs.MediaType)
+ return req
+ }
+ decodeResponse := func(t *testing.T, b *bytes.Buffer) *lfs.BatchResponse {
+ var br lfs.BatchResponse
+
+ assert.NoError(t, json.Unmarshal(b.Bytes(), &br))
+ return &br
+ }
+
+ t.Run("InvalidJsonRequest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, nil)
+
+ session.MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("InvalidOperation", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "dummy",
+ })
+
+ session.MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("InvalidPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "download",
+ Objects: []lfs.Pointer{
+ {Oid: "dummy"},
+ {Oid: oid, Size: -1},
+ },
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 2)
+ assert.Equal(t, "dummy", br.Objects[0].Oid)
+ assert.Equal(t, oid, br.Objects[1].Oid)
+ assert.Equal(t, int64(0), br.Objects[0].Size)
+ assert.Equal(t, int64(-1), br.Objects[1].Size)
+ assert.NotNil(t, br.Objects[0].Error)
+ assert.NotNil(t, br.Objects[1].Error)
+ assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code)
+ assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[1].Error.Code)
+ assert.Equal(t, "Oid or size are invalid", br.Objects[0].Error.Message)
+ assert.Equal(t, "Oid or size are invalid", br.Objects[1].Error.Message)
+ })
+
+ t.Run("PointerSizeMismatch", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "download",
+ Objects: []lfs.Pointer{
+ {Oid: oid, Size: 1},
+ },
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.NotNil(t, br.Objects[0].Error)
+ assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code)
+ assert.Equal(t, "Object "+oid+" is not 1 bytes", br.Objects[0].Error.Message)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("PointerNotInStore", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "download",
+ Objects: []lfs.Pointer{
+ {Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab042", Size: 6},
+ },
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.NotNil(t, br.Objects[0].Error)
+ assert.Equal(t, http.StatusNotFound, br.Objects[0].Error.Code)
+ })
+
+ t.Run("MetaNotFound", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ p := lfs.Pointer{Oid: "05eeb4eb5be71f2dd291ca39157d6d9effd7d1ea19cbdc8a99411fe2a8f26a00", Size: 6}
+
+ contentStore := lfs.NewContentStore()
+ exist, err := contentStore.Exists(p)
+ assert.NoError(t, err)
+ assert.False(t, exist)
+ err = contentStore.Put(p, bytes.NewReader([]byte("dummy0")))
+ assert.NoError(t, err)
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "download",
+ Objects: []lfs.Pointer{p},
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.NotNil(t, br.Objects[0].Error)
+ assert.Equal(t, http.StatusNotFound, br.Objects[0].Error.Code)
+ })
+
+ t.Run("Success", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "download",
+ Objects: []lfs.Pointer{
+ {Oid: oid, Size: 6},
+ },
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.Nil(t, br.Objects[0].Error)
+ assert.Contains(t, br.Objects[0].Actions, "download")
+ l := br.Objects[0].Actions["download"]
+ assert.NotNil(t, l)
+ assert.NotEmpty(t, l.Href)
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("FileTooBig", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ oldMaxFileSize := setting.LFS.MaxFileSize
+ setting.LFS.MaxFileSize = 2
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "upload",
+ Objects: []lfs.Pointer{
+ {Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab042", Size: 6},
+ },
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.NotNil(t, br.Objects[0].Error)
+ assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code)
+ assert.Equal(t, "Size must be less than or equal to 2", br.Objects[0].Error.Message)
+
+ setting.LFS.MaxFileSize = oldMaxFileSize
+ })
+
+ t.Run("AddMeta", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ p := lfs.Pointer{Oid: "05eeb4eb5be71f2dd291ca39157d6d9effd7d1ea19cbdc8a99411fe2a8f26a00", Size: 6}
+
+ contentStore := lfs.NewContentStore()
+ exist, err := contentStore.Exists(p)
+ assert.NoError(t, err)
+ assert.True(t, exist)
+
+ repo2 := createLFSTestRepository(t, "batch2")
+ content := []byte("dummy0")
+ storeObjectInRepo(t, repo2.ID, &content)
+
+ meta, err := git_model.GetLFSMetaObjectByOid(repo.ID, p.Oid)
+ assert.Nil(t, meta)
+ assert.Equal(t, git_model.ErrLFSObjectNotExist, err)
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "upload",
+ Objects: []lfs.Pointer{p},
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.Nil(t, br.Objects[0].Error)
+ assert.Empty(t, br.Objects[0].Actions)
+
+ meta, err = git_model.GetLFSMetaObjectByOid(repo.ID, p.Oid)
+ assert.NoError(t, err)
+ assert.NotNil(t, meta)
+
+ // Cleanup
+ err = contentStore.Delete(p.RelativePath())
+ assert.NoError(t, err)
+ })
+
+ t.Run("AlreadyExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "upload",
+ Objects: []lfs.Pointer{
+ {Oid: oid, Size: 6},
+ },
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.Nil(t, br.Objects[0].Error)
+ assert.Empty(t, br.Objects[0].Actions)
+ })
+
+ t.Run("NewFile", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.BatchRequest{
+ Operation: "upload",
+ Objects: []lfs.Pointer{
+ {Oid: "d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0153", Size: 1},
+ },
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ br := decodeResponse(t, resp.Body)
+ assert.Len(t, br.Objects, 1)
+ assert.Nil(t, br.Objects[0].Error)
+ assert.Contains(t, br.Objects[0].Actions, "upload")
+ ul := br.Objects[0].Actions["upload"]
+ assert.NotNil(t, ul)
+ assert.NotEmpty(t, ul.Href)
+ assert.Contains(t, br.Objects[0].Actions, "verify")
+ vl := br.Objects[0].Actions["verify"]
+ assert.NotNil(t, vl)
+ assert.NotEmpty(t, vl.Href)
+ })
+ })
+}
+
+func TestAPILFSUpload(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.LFS.StartServer = true
+
+ repo := createLFSTestRepository(t, "upload")
+
+ content := []byte("dummy3")
+ oid := storeObjectInRepo(t, repo.ID, &content)
+ defer git_model.RemoveLFSMetaObjectByOid(repo.ID, oid)
+
+ session := loginUser(t, "user2")
+
+ newRequest := func(t testing.TB, p lfs.Pointer, content string) *http.Request {
+ req := NewRequestWithBody(t, "PUT", path.Join("/user2/lfs-upload-repo.git/info/lfs/objects/", p.Oid, strconv.FormatInt(p.Size, 10)), strings.NewReader(content))
+ return req
+ }
+
+ t.Run("InvalidPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, lfs.Pointer{Oid: "dummy"}, "")
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+
+ t.Run("AlreadyExistsInStore", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ p := lfs.Pointer{Oid: "83de2e488b89a0aa1c97496b888120a28b0c1e15463a4adb8405578c540f36d4", Size: 6}
+
+ contentStore := lfs.NewContentStore()
+ exist, err := contentStore.Exists(p)
+ assert.NoError(t, err)
+ assert.False(t, exist)
+ err = contentStore.Put(p, bytes.NewReader([]byte("dummy5")))
+ assert.NoError(t, err)
+
+ meta, err := git_model.GetLFSMetaObjectByOid(repo.ID, p.Oid)
+ assert.Nil(t, meta)
+ assert.Equal(t, git_model.ErrLFSObjectNotExist, err)
+
+ t.Run("InvalidAccess", func(t *testing.T) {
+ req := newRequest(t, p, "invalid")
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+
+ t.Run("ValidAccess", func(t *testing.T) {
+ req := newRequest(t, p, "dummy5")
+
+ session.MakeRequest(t, req, http.StatusOK)
+ meta, err = git_model.GetLFSMetaObjectByOid(repo.ID, p.Oid)
+ assert.NoError(t, err)
+ assert.NotNil(t, meta)
+ })
+
+ // Cleanup
+ err = contentStore.Delete(p.RelativePath())
+ assert.NoError(t, err)
+ })
+
+ t.Run("MetaAlreadyExists", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, lfs.Pointer{Oid: oid, Size: 6}, "")
+
+ session.MakeRequest(t, req, http.StatusOK)
+ })
+
+ t.Run("HashMismatch", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, lfs.Pointer{Oid: "2581dd7bbc1fe44726de4b7dd806a087a978b9c5aec0a60481259e34be09b06a", Size: 1}, "a")
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+
+ t.Run("SizeMismatch", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, lfs.Pointer{Oid: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", Size: 2}, "a")
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+
+ t.Run("Success", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ p := lfs.Pointer{Oid: "6ccce4863b70f258d691f59609d31b4502e1ba5199942d3bc5d35d17a4ce771d", Size: 5}
+
+ req := newRequest(t, p, "gitea")
+
+ session.MakeRequest(t, req, http.StatusOK)
+
+ contentStore := lfs.NewContentStore()
+ exist, err := contentStore.Exists(p)
+ assert.NoError(t, err)
+ assert.True(t, exist)
+
+ meta, err := git_model.GetLFSMetaObjectByOid(repo.ID, p.Oid)
+ assert.NoError(t, err)
+ assert.NotNil(t, meta)
+ })
+}
+
+func TestAPILFSVerify(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.LFS.StartServer = true
+
+ repo := createLFSTestRepository(t, "verify")
+
+ content := []byte("dummy3")
+ oid := storeObjectInRepo(t, repo.ID, &content)
+ defer git_model.RemoveLFSMetaObjectByOid(repo.ID, oid)
+
+ session := loginUser(t, "user2")
+
+ newRequest := func(t testing.TB, p *lfs.Pointer) *http.Request {
+ req := NewRequestWithJSON(t, "POST", "/user2/lfs-verify-repo.git/info/lfs/verify", p)
+ req.Header.Set("Accept", lfs.MediaType)
+ req.Header.Set("Content-Type", lfs.MediaType)
+ return req
+ }
+
+ t.Run("InvalidJsonRequest", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, nil)
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+
+ t.Run("InvalidPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.Pointer{})
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+ })
+
+ t.Run("PointerNotExisting", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.Pointer{Oid: "fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab042", Size: 6})
+
+ session.MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("Success", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := newRequest(t, &lfs.Pointer{Oid: oid, Size: 6})
+
+ session.MakeRequest(t, req, http.StatusOK)
+ })
+}
diff --git a/tests/integration/api_repo_raw_test.go b/tests/integration/api_repo_raw_test.go
new file mode 100644
index 0000000000..9793e12b42
--- /dev/null
+++ b/tests/integration/api_repo_raw_test.go
@@ -0,0 +1,38 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIReposRaw(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ for _, ref := range [...]string{
+ "master", // Branch
+ "v1.1", // Tag
+ "65f1bf27bc3bf70f64657658635e66094edbcb4d", // Commit
+ } {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/raw/%s/README.md?token="+token, user.Name, ref)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ assert.EqualValues(t, "file", resp.Header().Get("x-gitea-object-type"))
+ }
+ // Test default branch
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/raw/README.md?token="+token, user.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ assert.EqualValues(t, "file", resp.Header().Get("x-gitea-object-type"))
+}
diff --git a/tests/integration/api_repo_tags_test.go b/tests/integration/api_repo_tags_test.go
new file mode 100644
index 0000000000..5d3a209a76
--- /dev/null
+++ b/tests/integration/api_repo_tags_test.go
@@ -0,0 +1,84 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIRepoTags(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ // Login as User2.
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ repoName := "repo1"
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/tags?token=%s", user.Name, repoName, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var tags []*api.Tag
+ DecodeJSON(t, resp, &tags)
+
+ assert.Len(t, tags, 1)
+ assert.Equal(t, "v1.1", tags[0].Name)
+ assert.Equal(t, "Initial commit", tags[0].Message)
+ assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.SHA)
+ assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.URL)
+ assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.zip", tags[0].ZipballURL)
+ assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.tar.gz", tags[0].TarballURL)
+
+ newTag := createNewTagUsingAPI(t, session, token, user.Name, repoName, "gitea/22", "", "nice!\nand some text")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &tags)
+ assert.Len(t, tags, 2)
+ for _, tag := range tags {
+ if tag.Name != "v1.1" {
+ assert.EqualValues(t, newTag.Name, tag.Name)
+ assert.EqualValues(t, newTag.Message, tag.Message)
+ assert.EqualValues(t, "nice!\nand some text", tag.Message)
+ assert.EqualValues(t, newTag.Commit.SHA, tag.Commit.SHA)
+ }
+ }
+
+ // get created tag
+ req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, newTag.Name, token)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var tag *api.Tag
+ DecodeJSON(t, resp, &tag)
+ assert.EqualValues(t, newTag, tag)
+
+ // delete tag
+ delReq := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, newTag.Name, token)
+ session.MakeRequest(t, delReq, http.StatusNoContent)
+
+ // check if it's gone
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
+
+func createNewTagUsingAPI(t *testing.T, session *TestSession, token, ownerName, repoName, name, target, msg string) *api.Tag {
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags?token=%s", ownerName, repoName, token)
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateTagOption{
+ TagName: name,
+ Message: msg,
+ Target: target,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var respObj api.Tag
+ DecodeJSON(t, resp, &respObj)
+ return &respObj
+}
diff --git a/tests/integration/api_repo_teams_test.go b/tests/integration/api_repo_teams_test.go
new file mode 100644
index 0000000000..1e476a89e2
--- /dev/null
+++ b/tests/integration/api_repo_teams_test.go
@@ -0,0 +1,82 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIRepoTeams(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // publicOrgRepo = user3/repo21
+ publicOrgRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32})
+ // user4
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ // ListTeams
+ url := fmt.Sprintf("/api/v1/repos/%s/teams?token=%s", publicOrgRepo.FullName(), token)
+ req := NewRequest(t, "GET", url)
+ res := session.MakeRequest(t, req, http.StatusOK)
+ var teams []*api.Team
+ DecodeJSON(t, res, &teams)
+ if assert.Len(t, teams, 2) {
+ assert.EqualValues(t, "Owners", teams[0].Name)
+ assert.True(t, teams[0].CanCreateOrgRepo)
+ assert.True(t, util.IsEqualSlice(unit.AllUnitKeyNames(), teams[0].Units), fmt.Sprintf("%v == %v", unit.AllUnitKeyNames(), teams[0].Units))
+ assert.EqualValues(t, "owner", teams[0].Permission)
+
+ assert.EqualValues(t, "test_team", teams[1].Name)
+ assert.False(t, teams[1].CanCreateOrgRepo)
+ assert.EqualValues(t, []string{"repo.issues"}, teams[1].Units)
+ assert.EqualValues(t, "write", teams[1].Permission)
+ }
+
+ // IsTeam
+ url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "Test_Team", token)
+ req = NewRequest(t, "GET", url)
+ res = session.MakeRequest(t, req, http.StatusOK)
+ var team *api.Team
+ DecodeJSON(t, res, &team)
+ assert.EqualValues(t, teams[1], team)
+
+ url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "NonExistingTeam", token)
+ req = NewRequest(t, "GET", url)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // AddTeam with user4
+ url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "team1", token)
+ req = NewRequest(t, "PUT", url)
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // AddTeam with user2
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session = loginUser(t, user.Name)
+ token = getTokenForLoggedInUser(t, session)
+ url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "team1", token)
+ req = NewRequest(t, "PUT", url)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity) // test duplicate request
+
+ // DeleteTeam
+ url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "team1", token)
+ req = NewRequest(t, "DELETE", url)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity) // test duplicate request
+}
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
new file mode 100644
index 0000000000..483503ccbb
--- /dev/null
+++ b/tests/integration/api_repo_test.go
@@ -0,0 +1,694 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIUserReposNotLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ req := NewRequestf(t, "GET", "/api/v1/users/%s/repos", user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var apiRepos []api.Repository
+ DecodeJSON(t, resp, &apiRepos)
+ expectedLen := unittest.GetCount(t, repo_model.Repository{OwnerID: user.ID},
+ unittest.Cond("is_private = ?", false))
+ assert.Len(t, apiRepos, expectedLen)
+ for _, repo := range apiRepos {
+ assert.EqualValues(t, user.ID, repo.Owner.ID)
+ assert.False(t, repo.Private)
+ }
+}
+
+func TestAPISearchRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ const keyword = "test"
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/search?q=%s", keyword)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var body api.SearchResults
+ DecodeJSON(t, resp, &body)
+ assert.NotEmpty(t, body.Data)
+ for _, repo := range body.Data {
+ assert.Contains(t, repo.Name, keyword)
+ assert.False(t, repo.Private)
+ }
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 18})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20})
+ orgUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
+
+ oldAPIDefaultNum := setting.API.DefaultPagingNum
+ defer func() {
+ setting.API.DefaultPagingNum = oldAPIDefaultNum
+ }()
+ setting.API.DefaultPagingNum = 10
+
+ // Map of expected results, where key is user for login
+ type expectedResults map[*user_model.User]struct {
+ count int
+ repoOwnerID int64
+ repoName string
+ includesPrivate bool
+ }
+
+ testCases := []struct {
+ name, requestURL string
+ expectedResults
+ }{
+ {
+ name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{
+ nil: {count: 30},
+ user: {count: 30},
+ user2: {count: 30},
+ },
+ },
+ {
+ name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10&private=false", expectedResults: expectedResults{
+ nil: {count: 10},
+ user: {count: 10},
+ user2: {count: 10},
+ },
+ },
+ {
+ name: "RepositoriesDefault", requestURL: "/api/v1/repos/search?default&private=false", expectedResults: expectedResults{
+ nil: {count: 10},
+ user: {count: 10},
+ user2: {count: 10},
+ },
+ },
+ {
+ name: "RepositoriesByName", requestURL: fmt.Sprintf("/api/v1/repos/search?q=%s&private=false", "big_test_"), expectedResults: expectedResults{
+ nil: {count: 7, repoName: "big_test_"},
+ user: {count: 7, repoName: "big_test_"},
+ user2: {count: 7, repoName: "big_test_"},
+ },
+ },
+ {
+ name: "RepositoriesByName", requestURL: fmt.Sprintf("/api/v1/repos/search?q=%s&private=false", "user2/big_test_"), expectedResults: expectedResults{
+ user2: {count: 2, repoName: "big_test_"},
+ },
+ },
+ {
+ name: "RepositoriesAccessibleAndRelatedToUser", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user.ID), expectedResults: expectedResults{
+ nil: {count: 5},
+ user: {count: 9, includesPrivate: true},
+ user2: {count: 6, includesPrivate: true},
+ },
+ },
+ {
+ name: "RepositoriesAccessibleAndRelatedToUser2", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user2.ID), expectedResults: expectedResults{
+ nil: {count: 1},
+ user: {count: 2, includesPrivate: true},
+ user2: {count: 2, includesPrivate: true},
+ user4: {count: 1},
+ },
+ },
+ {
+ name: "RepositoriesAccessibleAndRelatedToUser3", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user3.ID), expectedResults: expectedResults{
+ nil: {count: 1},
+ user: {count: 4, includesPrivate: true},
+ user2: {count: 3, includesPrivate: true},
+ user3: {count: 4, includesPrivate: true},
+ },
+ },
+ {
+ name: "RepositoriesOwnedByOrganization", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", orgUser.ID), expectedResults: expectedResults{
+ nil: {count: 1, repoOwnerID: orgUser.ID},
+ user: {count: 2, repoOwnerID: orgUser.ID, includesPrivate: true},
+ user2: {count: 1, repoOwnerID: orgUser.ID},
+ },
+ },
+ {name: "RepositoriesAccessibleAndRelatedToUser4", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user4.ID), expectedResults: expectedResults{
+ nil: {count: 3},
+ user: {count: 4, includesPrivate: true},
+ user4: {count: 7, includesPrivate: true},
+ }},
+ {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeSource", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "source"), expectedResults: expectedResults{
+ nil: {count: 0},
+ user: {count: 1, includesPrivate: true},
+ user4: {count: 1, includesPrivate: true},
+ }},
+ {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeFork", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "fork"), expectedResults: expectedResults{
+ nil: {count: 1},
+ user: {count: 1},
+ user4: {count: 2, includesPrivate: true},
+ }},
+ {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeFork/Exclusive", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s&exclusive=1", user4.ID, "fork"), expectedResults: expectedResults{
+ nil: {count: 1},
+ user: {count: 1},
+ user4: {count: 2, includesPrivate: true},
+ }},
+ {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeMirror", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "mirror"), expectedResults: expectedResults{
+ nil: {count: 2},
+ user: {count: 2},
+ user4: {count: 4, includesPrivate: true},
+ }},
+ {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeMirror/Exclusive", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s&exclusive=1", user4.ID, "mirror"), expectedResults: expectedResults{
+ nil: {count: 1},
+ user: {count: 1},
+ user4: {count: 2, includesPrivate: true},
+ }},
+ {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeCollaborative", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "collaborative"), expectedResults: expectedResults{
+ nil: {count: 0},
+ user: {count: 1, includesPrivate: true},
+ user4: {count: 1, includesPrivate: true},
+ }},
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ for userToLogin, expected := range testCase.expectedResults {
+ var session *TestSession
+ var testName string
+ var userID int64
+ var token string
+ if userToLogin != nil && userToLogin.ID > 0 {
+ testName = fmt.Sprintf("LoggedUser%d", userToLogin.ID)
+ session = loginUser(t, userToLogin.Name)
+ token = getTokenForLoggedInUser(t, session)
+ userID = userToLogin.ID
+ } else {
+ testName = "AnonymousUser"
+ session = emptyTestSession(t)
+ }
+
+ t.Run(testName, func(t *testing.T) {
+ request := NewRequest(t, "GET", testCase.requestURL+"&token="+token)
+ response := session.MakeRequest(t, request, http.StatusOK)
+
+ var body api.SearchResults
+ DecodeJSON(t, response, &body)
+
+ repoNames := make([]string, 0, len(body.Data))
+ for _, repo := range body.Data {
+ repoNames = append(repoNames, fmt.Sprintf("%d:%s:%t", repo.ID, repo.FullName, repo.Private))
+ }
+ assert.Len(t, repoNames, expected.count)
+ for _, repo := range body.Data {
+ r := getRepo(t, repo.ID)
+ hasAccess, err := access_model.HasAccess(db.DefaultContext, userID, r)
+ assert.NoError(t, err, "Error when checking if User: %d has access to %s: %v", userID, repo.FullName, err)
+ assert.True(t, hasAccess, "User: %d does not have access to %s", userID, repo.FullName)
+
+ assert.NotEmpty(t, repo.Name)
+ assert.Equal(t, repo.Name, r.Name)
+
+ if len(expected.repoName) > 0 {
+ assert.Contains(t, repo.Name, expected.repoName)
+ }
+
+ if expected.repoOwnerID > 0 {
+ assert.Equal(t, expected.repoOwnerID, repo.Owner.ID)
+ }
+
+ if !expected.includesPrivate {
+ assert.False(t, repo.Private, "User: %d not expecting private repository: %s", userID, repo.FullName)
+ }
+ }
+ })
+ }
+ })
+ }
+}
+
+var repoCache = make(map[int64]*repo_model.Repository)
+
+func getRepo(t *testing.T, repoID int64) *repo_model.Repository {
+ if _, ok := repoCache[repoID]; !ok {
+ repoCache[repoID] = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
+ }
+ return repoCache[repoID]
+}
+
+func TestAPIViewRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ var repo api.Repository
+
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1")
+ resp := MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &repo)
+ assert.EqualValues(t, 1, repo.ID)
+ assert.EqualValues(t, "repo1", repo.Name)
+ assert.EqualValues(t, 2, repo.Releases)
+ assert.EqualValues(t, 1, repo.OpenIssues)
+ assert.EqualValues(t, 3, repo.OpenPulls)
+
+ req = NewRequest(t, "GET", "/api/v1/repos/user12/repo10")
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &repo)
+ assert.EqualValues(t, 10, repo.ID)
+ assert.EqualValues(t, "repo10", repo.Name)
+ assert.EqualValues(t, 1, repo.OpenPulls)
+ assert.EqualValues(t, 1, repo.Forks)
+
+ req = NewRequest(t, "GET", "/api/v1/repos/user5/repo4")
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &repo)
+ assert.EqualValues(t, 4, repo.ID)
+ assert.EqualValues(t, "repo4", repo.Name)
+ assert.EqualValues(t, 1, repo.Stars)
+}
+
+func TestAPIOrgRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+ // User3 is an Org. Check their repos.
+ sourceOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+
+ expectedResults := map[*user_model.User]struct {
+ count int
+ includesPrivate bool
+ }{
+ nil: {count: 1},
+ user: {count: 3, includesPrivate: true},
+ user2: {count: 3, includesPrivate: true},
+ user3: {count: 1},
+ }
+
+ for userToLogin, expected := range expectedResults {
+ var session *TestSession
+ var testName string
+ var token string
+ if userToLogin != nil && userToLogin.ID > 0 {
+ testName = fmt.Sprintf("LoggedUser%d", userToLogin.ID)
+ session = loginUser(t, userToLogin.Name)
+ token = getTokenForLoggedInUser(t, session)
+ } else {
+ testName = "AnonymousUser"
+ session = emptyTestSession(t)
+ }
+ t.Run(testName, func(t *testing.T) {
+ req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos?token="+token, sourceOrg.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiRepos []*api.Repository
+ DecodeJSON(t, resp, &apiRepos)
+ assert.Len(t, apiRepos, expected.count)
+ for _, repo := range apiRepos {
+ if !expected.includesPrivate {
+ assert.False(t, repo.Private)
+ }
+ }
+ })
+ }
+}
+
+func TestAPIGetRepoByIDUnauthorized(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/repositories/2?token="+token)
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
+
+func TestAPIRepoMigrate(t *testing.T) {
+ testCases := []struct {
+ ctxUserID, userID int64
+ cloneURL, repoName string
+ expectedStatus int
+ }{
+ {ctxUserID: 1, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-admin", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-own", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, userID: 1, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad", expectedStatus: http.StatusForbidden},
+ {ctxUserID: 2, userID: 3, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-org", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, userID: 6, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad-org", expectedStatus: http.StatusForbidden},
+ {ctxUserID: 2, userID: 3, cloneURL: "https://localhost:3000/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity},
+ {ctxUserID: 2, userID: 3, cloneURL: "https://10.0.0.1/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity},
+ }
+
+ defer tests.PrepareTestEnv(t)()
+ for _, testCase := range testCases {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate?token="+token, &api.MigrateRepoOptions{
+ CloneAddr: testCase.cloneURL,
+ RepoOwnerID: testCase.userID,
+ RepoName: testCase.repoName,
+ })
+ resp := MakeRequest(t, req, NoExpectedStatus)
+ if resp.Code == http.StatusUnprocessableEntity {
+ respJSON := map[string]string{}
+ DecodeJSON(t, resp, &respJSON)
+ switch respJSON["message"] {
+ case "Remote visit addressed rate limitation.":
+ t.Log("test hit github rate limitation")
+ case "You can not import from disallowed hosts.":
+ assert.EqualValues(t, "private-ip", testCase.repoName)
+ default:
+ assert.Failf(t, "unexpected error '%v' on url '%s'", respJSON["message"], testCase.cloneURL)
+ }
+ } else {
+ assert.EqualValues(t, testCase.expectedStatus, resp.Code)
+ }
+ }
+}
+
+func TestAPIRepoMigrateConflict(t *testing.T) {
+ onGiteaRun(t, testAPIRepoMigrateConflict)
+}
+
+func testAPIRepoMigrateConflict(t *testing.T, u *url.URL) {
+ username := "user2"
+ baseAPITestContext := NewAPITestContext(t, username, "repo1")
+
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("Existing", func(t *testing.T) {
+ httpContext := baseAPITestContext
+
+ httpContext.Reponame = "repo-tmp-17"
+ dstPath, err := os.MkdirTemp("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+ t.Run("CreateRepo", doAPICreateRepository(httpContext, false))
+
+ user, err := user_model.GetUserByName(db.DefaultContext, httpContext.Username)
+ assert.NoError(t, err)
+ userID := user.ID
+
+ cloneURL := "https://github.com/go-gitea/test_repo.git"
+
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate?token="+httpContext.Token,
+ &api.MigrateRepoOptions{
+ CloneAddr: cloneURL,
+ RepoOwnerID: userID,
+ RepoName: httpContext.Reponame,
+ })
+ resp := httpContext.Session.MakeRequest(t, req, http.StatusConflict)
+ respJSON := map[string]string{}
+ DecodeJSON(t, resp, &respJSON)
+ assert.Equal(t, "The repository with the same name already exists.", respJSON["message"])
+ })
+}
+
+// mirror-sync must fail with "400 (Bad Request)" when an attempt is made to
+// sync a non-mirror repository.
+func TestAPIMirrorSyncNonMirrorRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+
+ var repo api.Repository
+ req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1")
+ resp := MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &repo)
+ assert.EqualValues(t, false, repo.Mirror)
+
+ req = NewRequestf(t, "POST", "/api/v1/repos/user2/repo1/mirror-sync?token=%s", token)
+ resp = session.MakeRequest(t, req, http.StatusBadRequest)
+ errRespJSON := map[string]string{}
+ DecodeJSON(t, resp, &errRespJSON)
+ assert.Equal(t, "Repository is not a mirror", errRespJSON["message"])
+}
+
+func TestAPIOrgRepoCreate(t *testing.T) {
+ testCases := []struct {
+ ctxUserID int64
+ orgName, repoName string
+ expectedStatus int
+ }{
+ {ctxUserID: 1, orgName: "user3", repoName: "repo-admin", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, orgName: "user3", repoName: "repo-own", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, orgName: "user6", repoName: "repo-bad-org", expectedStatus: http.StatusForbidden},
+ {ctxUserID: 28, orgName: "user3", repoName: "repo-creator", expectedStatus: http.StatusCreated},
+ {ctxUserID: 28, orgName: "user6", repoName: "repo-not-creator", expectedStatus: http.StatusForbidden},
+ }
+
+ defer tests.PrepareTestEnv(t)()
+ for _, testCase := range testCases {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos?token="+token, testCase.orgName), &api.CreateRepoOption{
+ Name: testCase.repoName,
+ })
+ session.MakeRequest(t, req, testCase.expectedStatus)
+ }
+}
+
+func TestAPIRepoCreateConflict(t *testing.T) {
+ onGiteaRun(t, testAPIRepoCreateConflict)
+}
+
+func testAPIRepoCreateConflict(t *testing.T, u *url.URL) {
+ username := "user2"
+ baseAPITestContext := NewAPITestContext(t, username, "repo1")
+
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("Existing", func(t *testing.T) {
+ httpContext := baseAPITestContext
+
+ httpContext.Reponame = "repo-tmp-17"
+ dstPath, err := os.MkdirTemp("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+ t.Run("CreateRepo", doAPICreateRepository(httpContext, false))
+
+ req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos?token="+httpContext.Token,
+ &api.CreateRepoOption{
+ Name: httpContext.Reponame,
+ })
+ resp := httpContext.Session.MakeRequest(t, req, http.StatusConflict)
+ respJSON := map[string]string{}
+ DecodeJSON(t, resp, &respJSON)
+ assert.Equal(t, respJSON["message"], "The repository with the same name already exists.")
+ })
+}
+
+func TestAPIRepoTransfer(t *testing.T) {
+ testCases := []struct {
+ ctxUserID int64
+ newOwner string
+ teams *[]int64
+ expectedStatus int
+ }{
+ // Disclaimer for test story: "user1" is an admin, "user2" is normal user and part of in owner team of org "user3"
+ // Transfer to a user with teams in another org should fail
+ {ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden},
+ // Transfer to a user with non-existent team IDs should fail
+ {ctxUserID: 1, newOwner: "user2", teams: &[]int64{2}, expectedStatus: http.StatusUnprocessableEntity},
+ // Transfer should go through
+ {ctxUserID: 1, newOwner: "user3", teams: &[]int64{2}, expectedStatus: http.StatusAccepted},
+ // Let user transfer it back to himself
+ {ctxUserID: 2, newOwner: "user2", expectedStatus: http.StatusAccepted},
+ // And revert transfer
+ {ctxUserID: 2, newOwner: "user3", teams: &[]int64{2}, expectedStatus: http.StatusAccepted},
+ // Cannot start transfer to an existing repo
+ {ctxUserID: 2, newOwner: "user3", teams: nil, expectedStatus: http.StatusUnprocessableEntity},
+ // Start transfer, repo is now in pending transfer mode
+ {ctxUserID: 2, newOwner: "user6", teams: nil, expectedStatus: http.StatusCreated},
+ }
+
+ defer tests.PrepareTestEnv(t)()
+
+ // create repo to move
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ repoName := "moveME"
+ apiRepo := new(api.Repository)
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/repos?token=%s", token), &api.CreateRepoOption{
+ Name: repoName,
+ Description: "repo move around",
+ Private: false,
+ Readme: "Default",
+ AutoInit: true,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, apiRepo)
+
+ // start testing
+ for _, testCase := range testCases {
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
+ session = loginUser(t, user.Name)
+ token = getTokenForLoggedInUser(t, session)
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer?token=%s", repo.OwnerName, repo.Name, token), &api.TransferRepoOption{
+ NewOwner: testCase.newOwner,
+ TeamIDs: testCase.teams,
+ })
+ session.MakeRequest(t, req, testCase.expectedStatus)
+ }
+
+ // cleanup
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
+ _ = models.DeleteRepository(user, repo.OwnerID, repo.ID)
+}
+
+func transfer(t *testing.T) *repo_model.Repository {
+ // create repo to move
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ repoName := "moveME"
+ apiRepo := new(api.Repository)
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/repos?token=%s", token), &api.CreateRepoOption{
+ Name: repoName,
+ Description: "repo move around",
+ Private: false,
+ Readme: "Default",
+ AutoInit: true,
+ })
+
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, apiRepo)
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer?token=%s", repo.OwnerName, repo.Name, token), &api.TransferRepoOption{
+ NewOwner: "user4",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ return repo
+}
+
+func TestAPIAcceptTransfer(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := transfer(t)
+
+ // try to accept with not authorized user
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // try to accept repo that's not marked as transferred
+ req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/accept?token=%s", "user2", "repo1", token))
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // accept transfer
+ session = loginUser(t, "user4")
+ token = getTokenForLoggedInUser(t, session)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/accept?token=%s", repo.OwnerName, repo.Name, token))
+ resp := session.MakeRequest(t, req, http.StatusAccepted)
+ apiRepo := new(api.Repository)
+ DecodeJSON(t, resp, apiRepo)
+ assert.Equal(t, "user4", apiRepo.Owner.UserName)
+}
+
+func TestAPIRejectTransfer(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := transfer(t)
+
+ // try to reject with not authorized user
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
+ session.MakeRequest(t, req, http.StatusForbidden)
+
+ // try to reject repo that's not marked as transferred
+ req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", "user2", "repo1", token))
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // reject transfer
+ session = loginUser(t, "user4")
+ token = getTokenForLoggedInUser(t, session)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ apiRepo := new(api.Repository)
+ DecodeJSON(t, resp, apiRepo)
+ assert.Equal(t, "user2", apiRepo.Owner.UserName)
+}
+
+func TestAPIGenerateRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ templateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
+
+ // user
+ repo := new(api.Repository)
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{
+ Owner: user.Name,
+ Name: "new-repo",
+ Description: "test generate repo",
+ Private: false,
+ GitContent: true,
+ })
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, repo)
+
+ assert.Equal(t, "new-repo", repo.Name)
+
+ // org
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate?token=%s", templateRepo.OwnerName, templateRepo.Name, token), &api.GenerateRepoOption{
+ Owner: "user3",
+ Name: "new-repo",
+ Description: "test generate repo",
+ Private: false,
+ GitContent: true,
+ })
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, repo)
+
+ assert.Equal(t, "new-repo", repo.Name)
+}
+
+func TestAPIRepoGetReviewers(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/reviewers?token=%s", user.Name, repo.Name, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var reviewers []*api.User
+ DecodeJSON(t, resp, &reviewers)
+ assert.Len(t, reviewers, 4)
+}
+
+func TestAPIRepoGetAssignees(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees?token=%s", user.Name, repo.Name, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var assignees []*api.User
+ DecodeJSON(t, resp, &assignees)
+ assert.Len(t, assignees, 1)
+}
diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go
new file mode 100644
index 0000000000..4e1e293890
--- /dev/null
+++ b/tests/integration/api_repo_topic_test.go
@@ -0,0 +1,155 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPITopicSearch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ searchURL, _ := url.Parse("/api/v1/topics/search")
+ var topics struct {
+ TopicNames []*api.TopicResponse `json:"topics"`
+ }
+
+ query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
+
+ searchURL.RawQuery = query.Encode()
+ res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 4)
+ assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+ query.Add("q", "topic")
+ searchURL.RawQuery = query.Encode()
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 2)
+
+ query.Set("q", "database")
+ searchURL.RawQuery = query.Encode()
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ if assert.Len(t, topics.TopicNames, 1) {
+ assert.EqualValues(t, 2, topics.TopicNames[0].ID)
+ assert.EqualValues(t, "database", topics.TopicNames[0].Name)
+ assert.EqualValues(t, 1, topics.TopicNames[0].RepoCount)
+ }
+}
+
+func TestAPIRepoTopic(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of repo2
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of repo3
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // write access to repo 3
+ repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+
+ // Get user2's token
+ token2 := getUserToken(t, user2.Name)
+
+ // Test read topics using login
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/topics", user2.Name, repo2.Name)
+ req := NewRequest(t, "GET", url+"?token="+token2)
+ res := MakeRequest(t, req, http.StatusOK)
+ var topics *api.TopicName
+ DecodeJSON(t, res, &topics)
+ assert.ElementsMatch(t, []string{"topicname1", "topicname2"}, topics.TopicNames)
+
+ // Log out user2
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/topics?token=%s", user2.Name, repo2.Name, token2)
+
+ // Test delete a topic
+ req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Topicname1", token2)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ // Test add an existing topic
+ req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Golang", token2)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ // Test add a topic
+ req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "topicName3", token2)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ // Test read topics using token
+ req = NewRequest(t, "GET", url)
+ res = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.ElementsMatch(t, []string{"topicname2", "golang", "topicname3"}, topics.TopicNames)
+
+ // Test replace topics
+ newTopics := []string{" windows ", " ", "MAC "}
+ req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
+ Topics: newTopics,
+ })
+ MakeRequest(t, req, http.StatusNoContent)
+ req = NewRequest(t, "GET", url)
+ res = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.ElementsMatch(t, []string{"windows", "mac"}, topics.TopicNames)
+
+ // Test replace topics with something invalid
+ newTopics = []string{"topicname1", "topicname2", "topicname!"}
+ req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
+ Topics: newTopics,
+ })
+ MakeRequest(t, req, http.StatusUnprocessableEntity)
+ req = NewRequest(t, "GET", url)
+ res = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.ElementsMatch(t, []string{"windows", "mac"}, topics.TopicNames)
+
+ // Test with some topics multiple times, less than 25 unique
+ newTopics = []string{"t1", "t2", "t1", "t3", "t4", "t5", "t6", "t7", "t8", "t9", "t10", "t11", "t12", "t13", "t14", "t15", "t16", "17", "t18", "t19", "t20", "t21", "t22", "t23", "t24", "t25"}
+ req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
+ Topics: newTopics,
+ })
+ MakeRequest(t, req, http.StatusNoContent)
+ req = NewRequest(t, "GET", url)
+ res = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 25)
+
+ // Test writing more topics than allowed
+ newTopics = append(newTopics, "t26")
+ req = NewRequestWithJSON(t, "PUT", url, &api.RepoTopicOptions{
+ Topics: newTopics,
+ })
+ MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // Test add a topic when there is already maximum
+ req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "t26", token2)
+ MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ // Test delete a topic that repo doesn't have
+ req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/topics/%s?token=%s", user2.Name, repo2.Name, "Topicname1", token2)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ // Get user4's token
+ token4 := getUserToken(t, user4.Name)
+
+ // Test read topics with write access
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/topics?token=%s", user3.Name, repo3.Name, token4)
+ req = NewRequest(t, "GET", url)
+ res = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Empty(t, topics.TopicNames)
+
+ // Test add a topic to repo with write access (requires repo admin access)
+ req = NewRequestf(t, "PUT", "/api/v1/repos/%s/%s/topics/%s?token=%s", user3.Name, repo3.Name, "topicName", token4)
+ MakeRequest(t, req, http.StatusForbidden)
+}
diff --git a/tests/integration/api_settings_test.go b/tests/integration/api_settings_test.go
new file mode 100644
index 0000000000..b8da17b963
--- /dev/null
+++ b/tests/integration/api_settings_test.go
@@ -0,0 +1,65 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIExposedSettings(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ ui := new(api.GeneralUISettings)
+ req := NewRequest(t, "GET", "/api/v1/settings/ui")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &ui)
+ assert.Len(t, ui.AllowedReactions, len(setting.UI.Reactions))
+ assert.ElementsMatch(t, setting.UI.Reactions, ui.AllowedReactions)
+
+ apiSettings := new(api.GeneralAPISettings)
+ req = NewRequest(t, "GET", "/api/v1/settings/api")
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &apiSettings)
+ assert.EqualValues(t, &api.GeneralAPISettings{
+ MaxResponseItems: setting.API.MaxResponseItems,
+ DefaultPagingNum: setting.API.DefaultPagingNum,
+ DefaultGitTreesPerPage: setting.API.DefaultGitTreesPerPage,
+ DefaultMaxBlobSize: setting.API.DefaultMaxBlobSize,
+ }, apiSettings)
+
+ repo := new(api.GeneralRepoSettings)
+ req = NewRequest(t, "GET", "/api/v1/settings/repository")
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &repo)
+ assert.EqualValues(t, &api.GeneralRepoSettings{
+ MirrorsDisabled: !setting.Mirror.Enabled,
+ HTTPGitDisabled: setting.Repository.DisableHTTPGit,
+ MigrationsDisabled: setting.Repository.DisableMigrations,
+ TimeTrackingDisabled: false,
+ LFSDisabled: !setting.LFS.StartServer,
+ }, repo)
+
+ attachment := new(api.GeneralAttachmentSettings)
+ req = NewRequest(t, "GET", "/api/v1/settings/attachment")
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &attachment)
+ assert.EqualValues(t, &api.GeneralAttachmentSettings{
+ Enabled: setting.Attachment.Enabled,
+ AllowedTypes: setting.Attachment.AllowedTypes,
+ MaxFiles: setting.Attachment.MaxFiles,
+ MaxSize: setting.Attachment.MaxSize,
+ }, attachment)
+}
diff --git a/tests/integration/api_team_test.go b/tests/integration/api_team_test.go
new file mode 100644
index 0000000000..a667949c09
--- /dev/null
+++ b/tests/integration/api_team_test.go
@@ -0,0 +1,268 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "testing"
+
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/convert"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPITeam(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ teamUser := unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{})
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamUser.TeamID})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: teamUser.UID})
+
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamUser.TeamID)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiTeam api.Team
+ DecodeJSON(t, resp, &apiTeam)
+ assert.EqualValues(t, team.ID, apiTeam.ID)
+ assert.Equal(t, team.Name, apiTeam.Name)
+
+ // non team member user will not access the teams details
+ teamUser2 := unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: teamUser2.UID})
+
+ session = loginUser(t, user2.Name)
+ token = getTokenForLoggedInUser(t, session)
+ req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamUser.TeamID)
+ _ = session.MakeRequest(t, req, http.StatusForbidden)
+
+ req = NewRequestf(t, "GET", "/api/v1/teams/%d", teamUser.TeamID)
+ _ = session.MakeRequest(t, req, http.StatusUnauthorized)
+
+ // Get an admin user able to create, update and delete teams.
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ session = loginUser(t, user.Name)
+ token = getTokenForLoggedInUser(t, session)
+
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 6})
+
+ // Create team.
+ teamToCreate := &api.CreateTeamOption{
+ Name: "team1",
+ Description: "team one",
+ IncludesAllRepositories: true,
+ Permission: "write",
+ Units: []string{"repo.code", "repo.issues"},
+ }
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate)
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "CreateTeam1", &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
+ teamToCreate.Permission, teamToCreate.Units, nil)
+ checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
+ teamToCreate.Permission, teamToCreate.Units, nil)
+ teamID := apiTeam.ID
+
+ // Edit team.
+ editDescription := "team 1"
+ editFalse := false
+ teamToEdit := &api.EditTeamOption{
+ Name: "teamone",
+ Description: &editDescription,
+ Permission: "admin",
+ IncludesAllRepositories: &editFalse,
+ Units: []string{"repo.code", "repo.pulls", "repo.releases"},
+ }
+
+ req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "EditTeam1", &apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
+ teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
+ checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
+ teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
+
+ // Edit team Description only
+ editDescription = "first team"
+ teamToEditDesc := api.EditTeamOption{Description: &editDescription}
+ req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEditDesc)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "EditTeam1_DescOnly", &apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
+ teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
+ checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
+ teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
+
+ // Read team.
+ teamRead := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
+ assert.NoError(t, teamRead.GetUnits())
+ req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "ReadTeam1", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
+ teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
+
+ // Delete team.
+ req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ unittest.AssertNotExistsBean(t, &organization.Team{ID: teamID})
+
+ // create team again via UnitsMap
+ // Create team.
+ teamToCreate = &api.CreateTeamOption{
+ Name: "team2",
+ Description: "team two",
+ IncludesAllRepositories: true,
+ Permission: "write",
+ UnitsMap: map[string]string{"repo.code": "read", "repo.issues": "write", "repo.wiki": "none"},
+ }
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", org.Name, token), teamToCreate)
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "CreateTeam2", &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
+ "read", nil, teamToCreate.UnitsMap)
+ checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
+ "read", nil, teamToCreate.UnitsMap)
+ teamID = apiTeam.ID
+
+ // Edit team.
+ editDescription = "team 1"
+ editFalse = false
+ teamToEdit = &api.EditTeamOption{
+ Name: "teamtwo",
+ Description: &editDescription,
+ Permission: "write",
+ IncludesAllRepositories: &editFalse,
+ UnitsMap: map[string]string{"repo.code": "read", "repo.pulls": "read", "repo.releases": "write"},
+ }
+
+ req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "EditTeam2", &apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
+ "read", nil, teamToEdit.UnitsMap)
+ checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
+ "read", nil, teamToEdit.UnitsMap)
+
+ // Edit team Description only
+ editDescription = "second team"
+ teamToEditDesc = api.EditTeamOption{Description: &editDescription}
+ req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEditDesc)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "EditTeam2_DescOnly", &apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
+ "read", nil, teamToEdit.UnitsMap)
+ checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
+ "read", nil, teamToEdit.UnitsMap)
+
+ // Read team.
+ teamRead = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
+ req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ apiTeam = api.Team{}
+ DecodeJSON(t, resp, &apiTeam)
+ assert.NoError(t, teamRead.GetUnits())
+ checkTeamResponse(t, "ReadTeam2", &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
+ teamRead.AccessMode.String(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
+
+ // Delete team.
+ req = NewRequestf(t, "DELETE", "/api/v1/teams/%d?token="+token, teamID)
+ session.MakeRequest(t, req, http.StatusNoContent)
+ unittest.AssertNotExistsBean(t, &organization.Team{ID: teamID})
+}
+
+func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
+ t.Run(testName, func(t *testing.T) {
+ assert.Equal(t, name, apiTeam.Name, "name")
+ assert.Equal(t, description, apiTeam.Description, "description")
+ assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories")
+ assert.Equal(t, permission, apiTeam.Permission, "permission")
+ if units != nil {
+ sort.StringSlice(units).Sort()
+ sort.StringSlice(apiTeam.Units).Sort()
+ assert.EqualValues(t, units, apiTeam.Units, "units")
+ }
+ if unitsMap != nil {
+ assert.EqualValues(t, unitsMap, apiTeam.UnitsMap, "unitsMap")
+ }
+ })
+}
+
+func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: id})
+ assert.NoError(t, team.GetUnits(), "GetUnits")
+ apiTeam, err := convert.ToTeam(team)
+ assert.NoError(t, err)
+ checkTeamResponse(t, fmt.Sprintf("checkTeamBean/%s_%s", name, description), apiTeam, name, description, includesAllRepositories, permission, units, unitsMap)
+}
+
+type TeamSearchResults struct {
+ OK bool `json:"ok"`
+ Data []*api.Team `json:"data"`
+}
+
+func TestAPITeamSearch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
+
+ var results TeamSearchResults
+
+ token := getUserToken(t, user.Name)
+ req := NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s&token=%s", org.Name, "_team", token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &results)
+ assert.NotEmpty(t, results.Data)
+ assert.Len(t, results.Data, 1)
+ assert.Equal(t, "test_team", results.Data[0].Name)
+
+ // no access if not organization member
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+ token5 := getUserToken(t, user5.Name)
+
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s&token=%s", org.Name, "team", token5)
+ MakeRequest(t, req, http.StatusForbidden)
+}
+
+func TestAPIGetTeamRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
+ teamRepo := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 24})
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
+
+ var results api.Repository
+
+ token := getUserToken(t, user.Name)
+ req := NewRequestf(t, "GET", "/api/v1/teams/%d/repos/%s/?token=%s", team.ID, teamRepo.FullName(), token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &results)
+ assert.Equal(t, "big_test_private_4", teamRepo.Name)
+
+ // no access if not organization member
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+ token5 := getUserToken(t, user5.Name)
+
+ req = NewRequestf(t, "GET", "/api/v1/teams/%d/repos/%s/?token=%s", team.ID, teamRepo.FullName(), token5)
+ MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/api_team_user_test.go b/tests/integration/api_team_user_test.go
new file mode 100644
index 0000000000..b999b97a2b
--- /dev/null
+++ b/tests/integration/api_team_user_test.go
@@ -0,0 +1,46 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/convert"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPITeamUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ normalUsername := "user2"
+ session := loginUser(t, normalUsername)
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequest(t, "GET", "/api/v1/teams/1/members/user1?token="+token)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", "/api/v1/teams/1/members/user2?token="+token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var user2 *api.User
+ DecodeJSON(t, resp, &user2)
+ user2.Created = user2.Created.In(time.Local)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
+
+ expectedUser := convert.ToUser(user, user)
+
+ // test time via unix timestamp
+ assert.EqualValues(t, expectedUser.LastLogin.Unix(), user2.LastLogin.Unix())
+ assert.EqualValues(t, expectedUser.Created.Unix(), user2.Created.Unix())
+ expectedUser.LastLogin = user2.LastLogin
+ expectedUser.Created = user2.Created
+
+ assert.Equal(t, expectedUser, user2)
+}
diff --git a/tests/integration/api_token_test.go b/tests/integration/api_token_test.go
new file mode 100644
index 0000000000..023bf30179
--- /dev/null
+++ b/tests/integration/api_token_test.go
@@ -0,0 +1,66 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+)
+
+// TestAPICreateAndDeleteToken tests that token that was just created can be deleted
+func TestAPICreateAndDeleteToken(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ req := NewRequestWithJSON(t, "POST", "/api/v1/users/user1/tokens", map[string]string{
+ "name": "test-key-1",
+ })
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ var newAccessToken api.AccessToken
+ DecodeJSON(t, resp, &newAccessToken)
+ unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{
+ ID: newAccessToken.ID,
+ Name: newAccessToken.Name,
+ Token: newAccessToken.Token,
+ UID: user.ID,
+ })
+
+ req = NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", newAccessToken.ID)
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: newAccessToken.ID})
+
+ req = NewRequestWithJSON(t, "POST", "/api/v1/users/user1/tokens", map[string]string{
+ "name": "test-key-2",
+ })
+ req = AddBasicAuthHeader(req, user.Name)
+ resp = MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, &newAccessToken)
+
+ req = NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%s", newAccessToken.Name)
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: newAccessToken.ID})
+}
+
+// TestAPIDeleteMissingToken ensures that error is thrown when token not found
+func TestAPIDeleteMissingToken(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", unittest.NonexistentID)
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/api_user_email_test.go b/tests/integration/api_user_email_test.go
new file mode 100644
index 0000000000..7bd265187c
--- /dev/null
+++ b/tests/integration/api_user_email_test.go
@@ -0,0 +1,112 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIListEmails(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ normalUsername := "user2"
+ session := loginUser(t, normalUsername)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequest(t, "GET", "/api/v1/user/emails?token="+token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var emails []*api.Email
+ DecodeJSON(t, resp, &emails)
+
+ assert.EqualValues(t, []*api.Email{
+ {
+ Email: "user2@example.com",
+ Verified: true,
+ Primary: true,
+ },
+ {
+ Email: "user2-2@example.com",
+ Verified: false,
+ Primary: false,
+ },
+ }, emails)
+}
+
+func TestAPIAddEmail(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ normalUsername := "user2"
+ session := loginUser(t, normalUsername)
+ token := getTokenForLoggedInUser(t, session)
+
+ opts := api.CreateEmailOption{
+ Emails: []string{"user101@example.com"},
+ }
+
+ req := NewRequestWithJSON(t, "POST", "/api/v1/user/emails?token="+token, &opts)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+
+ opts = api.CreateEmailOption{
+ Emails: []string{"user2-3@example.com"},
+ }
+ req = NewRequestWithJSON(t, "POST", "/api/v1/user/emails?token="+token, &opts)
+ resp := session.MakeRequest(t, req, http.StatusCreated)
+
+ var emails []*api.Email
+ DecodeJSON(t, resp, &emails)
+ assert.EqualValues(t, []*api.Email{
+ {
+ Email: "user2-3@example.com",
+ Verified: true,
+ Primary: false,
+ },
+ }, emails)
+
+ opts = api.CreateEmailOption{
+ Emails: []string{"notAEmail"},
+ }
+ req = NewRequestWithJSON(t, "POST", "/api/v1/user/emails?token="+token, &opts)
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+}
+
+func TestAPIDeleteEmail(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ normalUsername := "user2"
+ session := loginUser(t, normalUsername)
+ token := getTokenForLoggedInUser(t, session)
+
+ opts := api.DeleteEmailOption{
+ Emails: []string{"user2-3@example.com"},
+ }
+ req := NewRequestWithJSON(t, "DELETE", "/api/v1/user/emails?token="+token, &opts)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ opts = api.DeleteEmailOption{
+ Emails: []string{"user2-2@example.com"},
+ }
+ req = NewRequestWithJSON(t, "DELETE", "/api/v1/user/emails?token="+token, &opts)
+ session.MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "GET", "/api/v1/user/emails?token="+token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var emails []*api.Email
+ DecodeJSON(t, resp, &emails)
+ assert.EqualValues(t, []*api.Email{
+ {
+ Email: "user2@example.com",
+ Verified: true,
+ Primary: true,
+ },
+ }, emails)
+}
diff --git a/tests/integration/api_user_heatmap_test.go b/tests/integration/api_user_heatmap_test.go
new file mode 100644
index 0000000000..da6af0118d
--- /dev/null
+++ b/tests/integration/api_user_heatmap_test.go
@@ -0,0 +1,39 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.package models
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUserHeatmap(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ normalUsername := "user2"
+ token := getUserToken(t, adminUsername)
+
+ fakeNow := time.Date(2011, 10, 20, 0, 0, 0, 0, time.Local)
+ timeutil.Set(fakeNow)
+ defer timeutil.Unset()
+
+ urlStr := fmt.Sprintf("/api/v1/users/%s/heatmap?token=%s", normalUsername, token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := MakeRequest(t, req, http.StatusOK)
+ var heatmap []*activities_model.UserHeatmapData
+ DecodeJSON(t, resp, &heatmap)
+ var dummyheatmap []*activities_model.UserHeatmapData
+ dummyheatmap = append(dummyheatmap, &activities_model.UserHeatmapData{Timestamp: 1603227600, Contributions: 1})
+
+ assert.Equal(t, dummyheatmap, heatmap)
+}
diff --git a/tests/integration/api_user_org_perm_test.go b/tests/integration/api_user_org_perm_test.go
new file mode 100644
index 0000000000..fef653545c
--- /dev/null
+++ b/tests/integration/api_user_org_perm_test.go
@@ -0,0 +1,151 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type apiUserOrgPermTestCase struct {
+ LoginUser string
+ User string
+ Organization string
+ ExpectedOrganizationPermissions api.OrganizationPermissions
+}
+
+func TestTokenNeeded(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := emptyTestSession(t)
+ req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/user6/permissions")
+ session.MakeRequest(t, req, http.StatusUnauthorized)
+}
+
+func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, auoptc.LoginUser)
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/orgs/%s/permissions?token=%s", auoptc.User, auoptc.Organization, token))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var apiOP api.OrganizationPermissions
+ DecodeJSON(t, resp, &apiOP)
+ assert.Equal(t, auoptc.ExpectedOrganizationPermissions.IsOwner, apiOP.IsOwner)
+ assert.Equal(t, auoptc.ExpectedOrganizationPermissions.IsAdmin, apiOP.IsAdmin)
+ assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanWrite, apiOP.CanWrite)
+ assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanRead, apiOP.CanRead)
+ assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanCreateRepository, apiOP.CanCreateRepository)
+}
+
+func TestWithOwnerUser(t *testing.T) {
+ sampleTest(t, apiUserOrgPermTestCase{
+ LoginUser: "user2",
+ User: "user2",
+ Organization: "user3",
+ ExpectedOrganizationPermissions: api.OrganizationPermissions{
+ IsOwner: true,
+ IsAdmin: true,
+ CanWrite: true,
+ CanRead: true,
+ CanCreateRepository: true,
+ },
+ })
+}
+
+func TestCanWriteUser(t *testing.T) {
+ sampleTest(t, apiUserOrgPermTestCase{
+ LoginUser: "user4",
+ User: "user4",
+ Organization: "user3",
+ ExpectedOrganizationPermissions: api.OrganizationPermissions{
+ IsOwner: false,
+ IsAdmin: false,
+ CanWrite: true,
+ CanRead: true,
+ CanCreateRepository: false,
+ },
+ })
+}
+
+func TestAdminUser(t *testing.T) {
+ sampleTest(t, apiUserOrgPermTestCase{
+ LoginUser: "user1",
+ User: "user28",
+ Organization: "user3",
+ ExpectedOrganizationPermissions: api.OrganizationPermissions{
+ IsOwner: false,
+ IsAdmin: true,
+ CanWrite: true,
+ CanRead: true,
+ CanCreateRepository: true,
+ },
+ })
+}
+
+func TestAdminCanNotCreateRepo(t *testing.T) {
+ sampleTest(t, apiUserOrgPermTestCase{
+ LoginUser: "user1",
+ User: "user28",
+ Organization: "user6",
+ ExpectedOrganizationPermissions: api.OrganizationPermissions{
+ IsOwner: false,
+ IsAdmin: true,
+ CanWrite: true,
+ CanRead: true,
+ CanCreateRepository: false,
+ },
+ })
+}
+
+func TestCanReadUser(t *testing.T) {
+ sampleTest(t, apiUserOrgPermTestCase{
+ LoginUser: "user1",
+ User: "user24",
+ Organization: "org25",
+ ExpectedOrganizationPermissions: api.OrganizationPermissions{
+ IsOwner: false,
+ IsAdmin: false,
+ CanWrite: false,
+ CanRead: true,
+ CanCreateRepository: false,
+ },
+ })
+}
+
+func TestUnknowUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/unknow/orgs/org25/permissions?token=%s", token))
+ resp := session.MakeRequest(t, req, http.StatusNotFound)
+
+ var apiError api.APIError
+ DecodeJSON(t, resp, &apiError)
+ assert.Equal(t, "user redirect does not exist [name: unknow]", apiError.Message)
+}
+
+func TestUnknowOrganization(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/user1/orgs/unknow/permissions?token=%s", token))
+ resp := session.MakeRequest(t, req, http.StatusNotFound)
+ var apiError api.APIError
+ DecodeJSON(t, resp, &apiError)
+ assert.Equal(t, "GetUserByName", apiError.Message)
+}
diff --git a/tests/integration/api_user_orgs_test.go b/tests/integration/api_user_orgs_test.go
new file mode 100644
index 0000000000..622dfdcf21
--- /dev/null
+++ b/tests/integration/api_user_orgs_test.go
@@ -0,0 +1,121 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.package models
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUserOrgs(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ normalUsername := "user2"
+ privateMemberUsername := "user4"
+ unrelatedUsername := "user5"
+
+ orgs := getUserOrgs(t, adminUsername, normalUsername)
+
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user3"})
+ user17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user17"})
+
+ assert.Equal(t, []*api.Organization{
+ {
+ ID: 17,
+ UserName: user17.Name,
+ FullName: user17.FullName,
+ AvatarURL: user17.AvatarLink(),
+ Description: "",
+ Website: "",
+ Location: "",
+ Visibility: "public",
+ },
+ {
+ ID: 3,
+ UserName: user3.Name,
+ FullName: user3.FullName,
+ AvatarURL: user3.AvatarLink(),
+ Description: "",
+ Website: "",
+ Location: "",
+ Visibility: "public",
+ },
+ }, orgs)
+
+ // user itself should get it's org's he is a member of
+ orgs = getUserOrgs(t, privateMemberUsername, privateMemberUsername)
+ assert.Len(t, orgs, 1)
+
+ // unrelated user should not get private org membership of privateMemberUsername
+ orgs = getUserOrgs(t, unrelatedUsername, privateMemberUsername)
+ assert.Len(t, orgs, 0)
+
+ // not authenticated call also should hide org membership
+ orgs = getUserOrgs(t, "", privateMemberUsername)
+ assert.Len(t, orgs, 0)
+}
+
+func getUserOrgs(t *testing.T, userDoer, userCheck string) (orgs []*api.Organization) {
+ token := ""
+ session := emptyTestSession(t)
+ if len(userDoer) != 0 {
+ session = loginUser(t, userDoer)
+ token = getTokenForLoggedInUser(t, session)
+ }
+ urlStr := fmt.Sprintf("/api/v1/users/%s/orgs?token=%s", userCheck, token)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &orgs)
+ return orgs
+}
+
+func TestMyOrgs(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := emptyTestSession(t)
+ req := NewRequest(t, "GET", "/api/v1/user/orgs")
+ session.MakeRequest(t, req, http.StatusUnauthorized)
+
+ normalUsername := "user2"
+ session = loginUser(t, normalUsername)
+ token := getTokenForLoggedInUser(t, session)
+ req = NewRequest(t, "GET", "/api/v1/user/orgs?token="+token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var orgs []*api.Organization
+ DecodeJSON(t, resp, &orgs)
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user3"})
+ user17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user17"})
+
+ assert.Equal(t, []*api.Organization{
+ {
+ ID: 17,
+ UserName: user17.Name,
+ FullName: user17.FullName,
+ AvatarURL: user17.AvatarLink(),
+ Description: "",
+ Website: "",
+ Location: "",
+ Visibility: "public",
+ },
+ {
+ ID: 3,
+ UserName: user3.Name,
+ FullName: user3.FullName,
+ AvatarURL: user3.AvatarLink(),
+ Description: "",
+ Website: "",
+ Location: "",
+ Visibility: "public",
+ },
+ }, orgs)
+}
diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go
new file mode 100644
index 0000000000..9e9276077b
--- /dev/null
+++ b/tests/integration/api_user_search_test.go
@@ -0,0 +1,94 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.package models
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type SearchResults struct {
+ OK bool `json:"ok"`
+ Data []*api.User `json:"data"`
+}
+
+func TestAPIUserSearchLoggedIn(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ session := loginUser(t, adminUsername)
+ token := getTokenForLoggedInUser(t, session)
+ query := "user2"
+ req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var results SearchResults
+ DecodeJSON(t, resp, &results)
+ assert.NotEmpty(t, results.Data)
+ for _, user := range results.Data {
+ assert.Contains(t, user.UserName, query)
+ assert.NotEmpty(t, user.Email)
+ }
+}
+
+func TestAPIUserSearchNotLoggedIn(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ query := "user2"
+ req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var results SearchResults
+ DecodeJSON(t, resp, &results)
+ assert.NotEmpty(t, results.Data)
+ var modelUser *user_model.User
+ for _, user := range results.Data {
+ assert.Contains(t, user.UserName, query)
+ modelUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID})
+ if modelUser.KeepEmailPrivate {
+ assert.EqualValues(t, fmt.Sprintf("%s@%s", modelUser.LowerName, setting.Service.NoReplyAddress), user.Email)
+ } else {
+ assert.EqualValues(t, modelUser.Email, user.Email)
+ }
+ }
+}
+
+func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminUsername := "user1"
+ session := loginUser(t, adminUsername)
+ token := getTokenForLoggedInUser(t, session)
+ query := "user31"
+ req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query)
+ req.SetBasicAuth(token, "x-oauth-basic")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var results SearchResults
+ DecodeJSON(t, resp, &results)
+ assert.NotEmpty(t, results.Data)
+ for _, user := range results.Data {
+ assert.Contains(t, user.UserName, query)
+ assert.NotEmpty(t, user.Email)
+ assert.EqualValues(t, "private", user.Visibility)
+ }
+}
+
+func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ query := "user31"
+ req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var results SearchResults
+ DecodeJSON(t, resp, &results)
+ assert.Empty(t, results.Data)
+}
diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go
new file mode 100644
index 0000000000..c6f4841d08
--- /dev/null
+++ b/tests/integration/api_wiki_test.go
@@ -0,0 +1,252 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIGetWikiPage(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ username := "user2"
+ session := loginUser(t, username)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/Home", username, "repo1")
+
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var page *api.WikiPage
+ DecodeJSON(t, resp, &page)
+
+ assert.Equal(t, &api.WikiPage{
+ WikiPageMetaData: &api.WikiPageMetaData{
+ Title: "Home",
+ HTMLURL: page.HTMLURL,
+ SubURL: "Home",
+ LastCommit: &api.WikiCommit{
+ ID: "2c54faec6c45d31c1abfaecdab471eac6633738a",
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Ethan Koenig",
+ Email: "ethantkoenig@gmail.com",
+ },
+ Date: "2017-11-27T04:31:18Z",
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Ethan Koenig",
+ Email: "ethantkoenig@gmail.com",
+ },
+ Date: "2017-11-27T04:31:18Z",
+ },
+ Message: "Add Home.md\n",
+ },
+ },
+ ContentBase64: base64.RawStdEncoding.EncodeToString(
+ []byte("# Home page\n\nThis is the home page!\n"),
+ ),
+ CommitCount: 1,
+ Sidebar: "",
+ Footer: "",
+ }, page)
+}
+
+func TestAPIListWikiPages(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ username := "user2"
+ session := loginUser(t, username)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/pages", username, "repo1")
+
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var meta []*api.WikiPageMetaData
+ DecodeJSON(t, resp, &meta)
+
+ dummymeta := []*api.WikiPageMetaData{
+ {
+ Title: "Home",
+ HTMLURL: meta[0].HTMLURL,
+ SubURL: "Home",
+ LastCommit: &api.WikiCommit{
+ ID: "2c54faec6c45d31c1abfaecdab471eac6633738a",
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Ethan Koenig",
+ Email: "ethantkoenig@gmail.com",
+ },
+ Date: "2017-11-27T04:31:18Z",
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Ethan Koenig",
+ Email: "ethantkoenig@gmail.com",
+ },
+ Date: "2017-11-27T04:31:18Z",
+ },
+ Message: "Add Home.md\n",
+ },
+ },
+ {
+ Title: "Page With Image",
+ HTMLURL: meta[1].HTMLURL,
+ SubURL: "Page-With-Image",
+ LastCommit: &api.WikiCommit{
+ ID: "0cf15c3f66ec8384480ed9c3cf87c9e97fbb0ec3",
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Gabriel Silva Simões",
+ Email: "simoes.sgabriel@gmail.com",
+ },
+ Date: "2019-01-25T01:41:55Z",
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Gabriel Silva Simões",
+ Email: "simoes.sgabriel@gmail.com",
+ },
+ Date: "2019-01-25T01:41:55Z",
+ },
+ Message: "Add jpeg.jpg and page with image\n",
+ },
+ },
+ {
+ Title: "Page With Spaced Name",
+ HTMLURL: meta[2].HTMLURL,
+ SubURL: "Page-With-Spaced-Name",
+ LastCommit: &api.WikiCommit{
+ ID: "c10d10b7e655b3dab1f53176db57c8219a5488d6",
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Gabriel Silva Simões",
+ Email: "simoes.sgabriel@gmail.com",
+ },
+ Date: "2019-01-25T01:39:51Z",
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Gabriel Silva Simões",
+ Email: "simoes.sgabriel@gmail.com",
+ },
+ Date: "2019-01-25T01:39:51Z",
+ },
+ Message: "Add page with spaced name\n",
+ },
+ },
+ {
+ Title: "Unescaped File",
+ HTMLURL: meta[3].HTMLURL,
+ SubURL: "Unescaped-File",
+ LastCommit: &api.WikiCommit{
+ ID: "0dca5bd9b5d7ef937710e056f575e86c0184ba85",
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "6543",
+ Email: "6543@obermui.de",
+ },
+ Date: "2021-07-19T16:42:46Z",
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "6543",
+ Email: "6543@obermui.de",
+ },
+ Date: "2021-07-19T16:42:46Z",
+ },
+ Message: "add unescaped file\n",
+ },
+ },
+ }
+
+ assert.Equal(t, dummymeta, meta)
+}
+
+func TestAPINewWikiPage(t *testing.T) {
+ for _, title := range []string{
+ "New page",
+ "&&&&",
+ } {
+ defer tests.PrepareTestEnv(t)()
+ username := "user2"
+ session := loginUser(t, username)
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new?token=%s", username, "repo1", token)
+
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{
+ Title: title,
+ ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")),
+ Message: "",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+ }
+}
+
+func TestAPIEditWikiPage(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ username := "user2"
+ session := loginUser(t, username)
+ token := getTokenForLoggedInUser(t, session)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/Page-With-Spaced-Name?token=%s", username, "repo1", token)
+
+ req := NewRequestWithJSON(t, "PATCH", urlStr, &api.CreateWikiPageOptions{
+ Title: "edited title",
+ ContentBase64: base64.StdEncoding.EncodeToString([]byte("Edited wiki page content for API unit tests")),
+ Message: "",
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestAPIListPageRevisions(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ username := "user2"
+ session := loginUser(t, username)
+
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/revisions/Home", username, "repo1")
+
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var revisions *api.WikiCommitList
+ DecodeJSON(t, resp, &revisions)
+
+ dummyrevisions := &api.WikiCommitList{
+ WikiCommits: []*api.WikiCommit{
+ {
+ ID: "2c54faec6c45d31c1abfaecdab471eac6633738a",
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Ethan Koenig",
+ Email: "ethantkoenig@gmail.com",
+ },
+ Date: "2017-11-27T04:31:18Z",
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Ethan Koenig",
+ Email: "ethantkoenig@gmail.com",
+ },
+ Date: "2017-11-27T04:31:18Z",
+ },
+ Message: "Add Home.md\n",
+ },
+ },
+ Count: 1,
+ }
+
+ assert.Equal(t, dummyrevisions, revisions)
+}
diff --git a/tests/integration/attachment_test.go b/tests/integration/attachment_test.go
new file mode 100644
index 0000000000..2d2c979f7b
--- /dev/null
+++ b/tests/integration/attachment_test.go
@@ -0,0 +1,134 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "image"
+ "image/png"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "strings"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func generateImg() bytes.Buffer {
+ // Generate image
+ myImage := image.NewRGBA(image.Rect(0, 0, 32, 32))
+ var buff bytes.Buffer
+ png.Encode(&buff, myImage)
+ return buff
+}
+
+func createAttachment(t *testing.T, session *TestSession, repoURL, filename string, buff bytes.Buffer, expectedStatus int) string {
+ body := &bytes.Buffer{}
+
+ // Setup multi-part
+ writer := multipart.NewWriter(body)
+ part, err := writer.CreateFormFile("file", filename)
+ assert.NoError(t, err)
+ _, err = io.Copy(part, &buff)
+ assert.NoError(t, err)
+ err = writer.Close()
+ assert.NoError(t, err)
+
+ csrf := GetCSRF(t, session, repoURL)
+
+ req := NewRequestWithBody(t, "POST", repoURL+"/issues/attachments", body)
+ req.Header.Add("X-Csrf-Token", csrf)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ resp := session.MakeRequest(t, req, expectedStatus)
+
+ if expectedStatus != http.StatusOK {
+ return ""
+ }
+ var obj map[string]string
+ DecodeJSON(t, resp, &obj)
+ return obj["uuid"]
+}
+
+func TestCreateAnonymousAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := emptyTestSession(t)
+ createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusSeeOther)
+}
+
+func TestCreateIssueAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ const repoURL = "user2/repo1"
+ session := loginUser(t, "user2")
+ uuid := createAttachment(t, session, repoURL, "image.png", generateImg(), http.StatusOK)
+
+ req := NewRequest(t, "GET", repoURL+"/issues/new")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action")
+ assert.True(t, exists, "The template has changed")
+
+ postData := map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "title": "New Issue With Attachment",
+ "content": "some content",
+ "files": uuid,
+ }
+
+ req = NewRequestWithValues(t, "POST", link, postData)
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ test.RedirectURL(resp) // check that redirect URL exists
+
+ // Validate that attachment is available
+ req = NewRequest(t, "GET", "/attachments/"+uuid)
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestGetAttachment(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ adminSession := loginUser(t, "user1")
+ user2Session := loginUser(t, "user2")
+ user8Session := loginUser(t, "user8")
+ emptySession := emptyTestSession(t)
+ testCases := []struct {
+ name string
+ uuid string
+ createFile bool
+ session *TestSession
+ want int
+ }{
+ {"LinkedIssueUUID", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", true, user2Session, http.StatusOK},
+ {"LinkedCommentUUID", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", true, user2Session, http.StatusOK},
+ {"linked_release_uuid", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19", true, user2Session, http.StatusOK},
+ {"NotExistingUUID", "b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18", false, user2Session, http.StatusNotFound},
+ {"FileMissing", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18", false, user2Session, http.StatusInternalServerError},
+ {"NotLinked", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20", true, user2Session, http.StatusNotFound},
+ {"NotLinkedAccessibleByUploader", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20", true, user8Session, http.StatusOK},
+ {"PublicByNonLogged", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", true, emptySession, http.StatusOK},
+ {"PrivateByNonLogged", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, emptySession, http.StatusNotFound},
+ {"PrivateAccessibleByAdmin", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, adminSession, http.StatusOK},
+ {"PrivateAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, user2Session, http.StatusOK},
+ {"RepoNotAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, user8Session, http.StatusNotFound},
+ {"OrgNotAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a21", true, user8Session, http.StatusNotFound},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Write empty file to be available for response
+ if tc.createFile {
+ _, err := storage.Attachments.Save(repo_model.AttachmentRelativePath(tc.uuid), strings.NewReader("hello world"), -1)
+ assert.NoError(t, err)
+ }
+ // Actual test
+ req := NewRequest(t, "GET", "/attachments/"+tc.uuid)
+ tc.session.MakeRequest(t, req, tc.want)
+ })
+ }
+}
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
new file mode 100644
index 0000000000..f3c3e6d7b3
--- /dev/null
+++ b/tests/integration/auth_ldap_test.go
@@ -0,0 +1,415 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type ldapUser struct {
+ UserName string
+ Password string
+ FullName string
+ Email string
+ OtherEmails []string
+ IsAdmin bool
+ IsRestricted bool
+ SSHKeys []string
+}
+
+var gitLDAPUsers = []ldapUser{
+ {
+ UserName: "professor",
+ Password: "professor",
+ FullName: "Hubert Farnsworth",
+ Email: "professor@planetexpress.com",
+ OtherEmails: []string{"hubert@planetexpress.com"},
+ IsAdmin: true,
+ },
+ {
+ UserName: "hermes",
+ Password: "hermes",
+ FullName: "Conrad Hermes",
+ Email: "hermes@planetexpress.com",
+ SSHKeys: []string{
+ "SHA256:qLY06smKfHoW/92yXySpnxFR10QFrLdRjf/GNPvwcW8",
+ "SHA256:QlVTuM5OssDatqidn2ffY+Lc4YA5Fs78U+0KOHI51jQ",
+ "SHA256:DXdeUKYOJCSSmClZuwrb60hUq7367j4fA+udNC3FdRI",
+ },
+ IsAdmin: true,
+ },
+ {
+ UserName: "fry",
+ Password: "fry",
+ FullName: "Philip Fry",
+ Email: "fry@planetexpress.com",
+ },
+ {
+ UserName: "leela",
+ Password: "leela",
+ FullName: "Leela Turanga",
+ Email: "leela@planetexpress.com",
+ IsRestricted: true,
+ },
+ {
+ UserName: "bender",
+ Password: "bender",
+ FullName: "Bender Rodríguez",
+ Email: "bender@planetexpress.com",
+ },
+}
+
+var otherLDAPUsers = []ldapUser{
+ {
+ UserName: "zoidberg",
+ Password: "zoidberg",
+ FullName: "John Zoidberg",
+ Email: "zoidberg@planetexpress.com",
+ },
+ {
+ UserName: "amy",
+ Password: "amy",
+ FullName: "Amy Kroker",
+ Email: "amy@planetexpress.com",
+ },
+}
+
+func skipLDAPTests() bool {
+ return os.Getenv("TEST_LDAP") != "1"
+}
+
+func getLDAPServerHost() string {
+ host := os.Getenv("TEST_LDAP_HOST")
+ if len(host) == 0 {
+ host = "ldap"
+ }
+ return host
+}
+
+func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string, groupMapParams ...string) {
+ groupTeamMapRemoval := "off"
+ groupTeamMap := ""
+ if len(groupMapParams) == 2 {
+ groupTeamMapRemoval = groupMapParams[0]
+ groupTeamMap = groupMapParams[1]
+ }
+ session := loginUser(t, "user1")
+ csrf := GetCSRF(t, session, "/admin/auths/new")
+ req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{
+ "_csrf": csrf,
+ "type": "2",
+ "name": "ldap",
+ "host": getLDAPServerHost(),
+ "port": "389",
+ "bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com",
+ "bind_password": "password",
+ "user_base": "ou=people,dc=planetexpress,dc=com",
+ "filter": "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))",
+ "admin_filter": "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
+ "restricted_filter": "(uid=leela)",
+ "attribute_username": "uid",
+ "attribute_name": "givenName",
+ "attribute_surname": "sn",
+ "attribute_mail": "mail",
+ "attribute_ssh_public_key": sshKeyAttribute,
+ "is_sync_enabled": "on",
+ "is_active": "on",
+ "groups_enabled": "on",
+ "group_dn": "ou=people,dc=planetexpress,dc=com",
+ "group_member_uid": "member",
+ "group_team_map": groupTeamMap,
+ "group_team_map_removal": groupTeamMapRemoval,
+ "user_uid": "DN",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
+func TestLDAPUserSignin(t *testing.T) {
+ if skipLDAPTests() {
+ t.Skip()
+ return
+ }
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "")
+
+ u := gitLDAPUsers[0]
+
+ session := loginUserWithPassword(t, u.UserName, u.Password)
+ req := NewRequest(t, "GET", "/user/settings")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
+ assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
+ assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
+}
+
+func TestLDAPAuthChange(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "")
+
+ session := loginUser(t, "user1")
+ req := NewRequest(t, "GET", "/admin/auths")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+ href, exists := doc.Find("table.table td a").Attr("href")
+ if !exists {
+ assert.True(t, exists, "No authentication source found")
+ return
+ }
+
+ req = NewRequest(t, "GET", href)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ doc = NewHTMLParser(t, resp.Body)
+ csrf := doc.GetCSRF()
+ host, _ := doc.Find(`input[name="host"]`).Attr("value")
+ assert.Equal(t, host, getLDAPServerHost())
+ binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
+ assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com")
+
+ req = NewRequestWithValues(t, "POST", href, map[string]string{
+ "_csrf": csrf,
+ "type": "2",
+ "name": "ldap",
+ "host": getLDAPServerHost(),
+ "port": "389",
+ "bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com",
+ "bind_password": "password",
+ "user_base": "ou=people,dc=planetexpress,dc=com",
+ "filter": "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))",
+ "admin_filter": "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
+ "restricted_filter": "(uid=leela)",
+ "attribute_username": "uid",
+ "attribute_name": "givenName",
+ "attribute_surname": "sn",
+ "attribute_mail": "mail",
+ "attribute_ssh_public_key": "",
+ "is_sync_enabled": "on",
+ "is_active": "on",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequest(t, "GET", href)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ doc = NewHTMLParser(t, resp.Body)
+ host, _ = doc.Find(`input[name="host"]`).Attr("value")
+ assert.Equal(t, host, getLDAPServerHost())
+ binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value")
+ assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com")
+}
+
+func TestLDAPUserSync(t *testing.T) {
+ if skipLDAPTests() {
+ t.Skip()
+ return
+ }
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "")
+ auth.SyncExternalUsers(context.Background(), true)
+
+ session := loginUser(t, "user1")
+ // Check if users exists
+ for _, u := range gitLDAPUsers {
+ req := NewRequest(t, "GET", "/admin/users?q="+u.UserName)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ tr := htmlDoc.doc.Find("table.table tbody tr")
+ if !assert.True(t, tr.Length() == 1) {
+ continue
+ }
+ tds := tr.Find("td")
+ if !assert.True(t, tds.Length() > 0) {
+ continue
+ }
+ assert.Equal(t, u.UserName, strings.TrimSpace(tds.Find("td:nth-child(2) a").Text()))
+ assert.Equal(t, u.Email, strings.TrimSpace(tds.Find("td:nth-child(3) span").Text()))
+ if u.IsAdmin {
+ assert.True(t, tds.Find("td:nth-child(5) svg").HasClass("octicon-check"))
+ } else {
+ assert.True(t, tds.Find("td:nth-child(5) svg").HasClass("octicon-x"))
+ }
+ if u.IsRestricted {
+ assert.True(t, tds.Find("td:nth-child(6) svg").HasClass("octicon-check"))
+ } else {
+ assert.True(t, tds.Find("td:nth-child(6) svg").HasClass("octicon-x"))
+ }
+ }
+
+ // Check if no users exist
+ for _, u := range otherLDAPUsers {
+ req := NewRequest(t, "GET", "/admin/users?q="+u.UserName)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ tr := htmlDoc.doc.Find("table.table tbody tr")
+ assert.True(t, tr.Length() == 0)
+ }
+}
+
+func TestLDAPUserSigninFailed(t *testing.T) {
+ if skipLDAPTests() {
+ t.Skip()
+ return
+ }
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "")
+
+ u := otherLDAPUsers[0]
+ testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect"))
+}
+
+func TestLDAPUserSSHKeySync(t *testing.T) {
+ if skipLDAPTests() {
+ t.Skip()
+ return
+ }
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "sshPublicKey")
+
+ auth.SyncExternalUsers(context.Background(), true)
+
+ // Check if users has SSH keys synced
+ for _, u := range gitLDAPUsers {
+ if len(u.SSHKeys) == 0 {
+ continue
+ }
+ session := loginUserWithPassword(t, u.UserName, u.Password)
+
+ req := NewRequest(t, "GET", "/user/settings/keys")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ divs := htmlDoc.doc.Find(".key.list .print.meta")
+
+ syncedKeys := make([]string, divs.Length())
+ for i := 0; i < divs.Length(); i++ {
+ syncedKeys[i] = strings.TrimSpace(divs.Eq(i).Text())
+ }
+
+ assert.ElementsMatch(t, u.SSHKeys, syncedKeys, "Unequal number of keys synchronized for user: %s", u.UserName)
+ }
+}
+
+func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
+ if skipLDAPTests() {
+ t.Skip()
+ return
+ }
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`)
+ org, err := organization.GetOrgByName("org26")
+ assert.NoError(t, err)
+ team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
+ assert.NoError(t, err)
+ auth.SyncExternalUsers(context.Background(), true)
+ for _, gitLDAPUser := range gitLDAPUsers {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: gitLDAPUser.UserName,
+ })
+ usersOrgs, err := organization.FindOrgs(organization.FindOrgOptions{
+ UserID: user.ID,
+ IncludePrivate: true,
+ })
+ assert.NoError(t, err)
+ allOrgTeams, err := organization.GetUserOrgTeams(db.DefaultContext, org.ID, user.ID)
+ assert.NoError(t, err)
+ if user.Name == "fry" || user.Name == "leela" || user.Name == "bender" {
+ // assert members of LDAP group "cn=ship_crew" are added to mapped teams
+ assert.Equal(t, len(usersOrgs), 1, "User [%s] should be member of one organization", user.Name)
+ assert.Equal(t, usersOrgs[0].Name, "org26", "Membership should be added to the right organization")
+ isMember, err := organization.IsTeamMember(db.DefaultContext, usersOrgs[0].ID, team.ID, user.ID)
+ assert.NoError(t, err)
+ assert.True(t, isMember, "Membership should be added to the right team")
+ err = models.RemoveTeamMember(team, user.ID)
+ assert.NoError(t, err)
+ err = models.RemoveOrgUser(usersOrgs[0].ID, user.ID)
+ assert.NoError(t, err)
+ } else {
+ // assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
+ assert.Empty(t, usersOrgs, "User should be member of no organization")
+ isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
+ assert.NoError(t, err)
+ assert.False(t, isMember, "User should no be added to this team")
+ assert.Empty(t, allOrgTeams, "User should not be added to any team")
+ }
+ }
+}
+
+func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
+ if skipLDAPTests() {
+ t.Skip()
+ return
+ }
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
+ org, err := organization.GetOrgByName("org26")
+ assert.NoError(t, err)
+ team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
+ assert.NoError(t, err)
+ loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: gitLDAPUsers[0].UserName,
+ })
+ err = organization.AddOrgUser(org.ID, user.ID)
+ assert.NoError(t, err)
+ err = models.AddTeamMember(team, user.ID)
+ assert.NoError(t, err)
+ isMember, err := organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
+ assert.NoError(t, err)
+ assert.True(t, isMember, "User should be member of this organization")
+ isMember, err = organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
+ assert.NoError(t, err)
+ assert.True(t, isMember, "User should be member of this team")
+ // assert team member "professor" gets removed from org26 team11
+ loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
+ isMember, err = organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
+ assert.NoError(t, err)
+ assert.False(t, isMember, "User membership should have been removed from organization")
+ isMember, err = organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID)
+ assert.NoError(t, err)
+ assert.False(t, isMember, "User membership should have been removed from team")
+}
+
+// Login should work even if Team Group Map contains a broken JSON
+func TestBrokenLDAPMapUserSignin(t *testing.T) {
+ if skipLDAPTests() {
+ t.Skip()
+ return
+ }
+ defer tests.PrepareTestEnv(t)()
+ addAuthSourceLDAP(t, "", "on", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`)
+
+ u := gitLDAPUsers[0]
+
+ session := loginUserWithPassword(t, u.UserName, u.Password)
+ req := NewRequest(t, "GET", "/user/settings")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
+ assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
+ assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
+}
diff --git a/tests/integration/benchmarks_test.go b/tests/integration/benchmarks_test.go
new file mode 100644
index 0000000000..bf66d221fb
--- /dev/null
+++ b/tests/integration/benchmarks_test.go
@@ -0,0 +1,72 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "math/rand"
+ "net/http"
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// StringWithCharset random string (from https://www.calhoun.io/creating-random-strings-in-go/)
+func StringWithCharset(length int, charset string) string {
+ b := make([]byte, length)
+ for i := range b {
+ b[i] = charset[rand.Intn(len(charset))]
+ }
+ return string(b)
+}
+
+func BenchmarkRepoBranchCommit(b *testing.B) {
+ onGiteaRunTB(b, func(t testing.TB, u *url.URL) {
+ b := t.(*testing.B)
+
+ samples := []int64{1, 2, 3}
+ b.ResetTimer()
+
+ for _, repoID := range samples {
+ b.StopTimer()
+ repo := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: repoID})
+ b.StartTimer()
+ b.Run(repo.Name, func(b *testing.B) {
+ session := loginUser(b, "user2")
+ b.ResetTimer()
+ b.Run("CreateBranch", func(b *testing.B) {
+ b.StopTimer()
+ branchName := StringWithCharset(5+rand.Intn(10), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ b.Run("new_"+branchName, func(b *testing.B) {
+ b.Skip("benchmark broken") // TODO fix
+ testAPICreateBranch(b, session, repo.OwnerName, repo.Name, repo.DefaultBranch, "new_"+branchName, http.StatusCreated)
+ })
+ }
+ })
+ b.Run("GetBranches", func(b *testing.B) {
+ req := NewRequestf(b, "GET", "/api/v1/repos/%s/branches", repo.FullName())
+ session.MakeRequest(b, req, http.StatusOK)
+ })
+ b.Run("AccessCommits", func(b *testing.B) {
+ var branches []*api.Branch
+ req := NewRequestf(b, "GET", "/api/v1/repos/%s/branches", repo.FullName())
+ resp := session.MakeRequest(b, req, http.StatusOK)
+ DecodeJSON(b, resp, &branches)
+ b.ResetTimer() // We measure from here
+ if len(branches) != 0 {
+ for i := 0; i < b.N; i++ {
+ req := NewRequestf(b, "GET", "/api/v1/repos/%s/commits?sha=%s", repo.FullName(), branches[i%len(branches)].Name)
+ session.MakeRequest(b, req, http.StatusOK)
+ }
+ }
+ })
+ })
+ }
+ })
+}
diff --git a/tests/integration/branches_test.go b/tests/integration/branches_test.go
new file mode 100644
index 0000000000..bd0bd63c50
--- /dev/null
+++ b/tests/integration/branches_test.go
@@ -0,0 +1,76 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViewBranches(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/branches")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ _, exists := htmlDoc.doc.Find(".delete-branch-button").Attr("data-url")
+ assert.False(t, exists, "The template has changed")
+}
+
+func TestDeleteBranch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ deleteBranch(t)
+}
+
+func TestUndoDeleteBranch(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ deleteBranch(t)
+ htmlDoc, name := branchAction(t, ".undo-button")
+ assert.Contains(t,
+ htmlDoc.doc.Find(".ui.positive.message").Text(),
+ translation.NewLocale("en-US").Tr("repo.branch.restore_success", name),
+ )
+ })
+}
+
+func deleteBranch(t *testing.T) {
+ htmlDoc, name := branchAction(t, ".delete-branch-button")
+ assert.Contains(t,
+ htmlDoc.doc.Find(".ui.positive.message").Text(),
+ translation.NewLocale("en-US").Tr("repo.branch.deletion_success", name),
+ )
+}
+
+func branchAction(t *testing.T, button string) (*HTMLDoc, string) {
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/user2/repo1/branches")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find(button).Attr("data-url")
+ if !assert.True(t, exists, "The template has changed") {
+ t.Skip()
+ }
+
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+
+ url, err := url.Parse(link)
+ assert.NoError(t, err)
+ req = NewRequest(t, "GET", "/user2/repo1/branches")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ return NewHTMLParser(t, resp.Body), url.Query().Get("name")
+}
diff --git a/tests/integration/change_default_branch_test.go b/tests/integration/change_default_branch_test.go
new file mode 100644
index 0000000000..8edc0e63c4
--- /dev/null
+++ b/tests/integration/change_default_branch_test.go
@@ -0,0 +1,41 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestChangeDefaultBranch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ branchesURL := fmt.Sprintf("/%s/%s/settings/branches", owner.Name, repo.Name)
+
+ csrf := GetCSRF(t, session, branchesURL)
+ req := NewRequestWithValues(t, "POST", branchesURL, map[string]string{
+ "_csrf": csrf,
+ "action": "default_branch",
+ "branch": "DefaultBranch",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ csrf = GetCSRF(t, session, branchesURL)
+ req = NewRequestWithValues(t, "POST", branchesURL, map[string]string{
+ "_csrf": csrf,
+ "action": "default_branch",
+ "branch": "does_not_exist",
+ })
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go
new file mode 100644
index 0000000000..0c72956c29
--- /dev/null
+++ b/tests/integration/cmd_keys_test.go
@@ -0,0 +1,65 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "flag"
+ "io"
+ "net/url"
+ "os"
+ "testing"
+
+ "code.gitea.io/gitea/cmd"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/urfave/cli"
+)
+
+func Test_CmdKeys(t *testing.T) {
+ onGiteaRun(t, func(*testing.T, *url.URL) {
+ tests := []struct {
+ name string
+ args []string
+ wantErr bool
+ expectedOutput string
+ }{
+ {"test_empty_1", []string{"keys", "--username=git", "--type=test", "--content=test"}, true, ""},
+ {"test_empty_2", []string{"keys", "-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""},
+ {
+ "with_key",
+ []string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
+ false,
+ "# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost\n",
+ },
+ {"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ realStdout := os.Stdout // Backup Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+
+ set := flag.NewFlagSet("keys", 0)
+ _ = set.Parse(tt.args)
+ context := cli.NewContext(&cli.App{Writer: os.Stdout}, set, nil)
+ err := cmd.CmdKeys.Run(context)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("CmdKeys.Run() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ w.Close()
+ var buf bytes.Buffer
+ io.Copy(&buf, r)
+ commandOutput := buf.String()
+ if tt.expectedOutput != commandOutput {
+ t.Errorf("expectedOutput: %#v, commandOutput: %#v", tt.expectedOutput, commandOutput)
+ }
+ // Restore stdout
+ os.Stdout = realStdout
+ })
+ }
+ })
+}
diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go
new file mode 100644
index 0000000000..7642109dd9
--- /dev/null
+++ b/tests/integration/compare_test.go
@@ -0,0 +1,42 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCompareTag(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/user2/repo1/compare/v1.1...master")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ selection := htmlDoc.doc.Find(".choose.branch .filter.dropdown")
+ // A dropdown for both base and head.
+ assert.Lenf(t, selection.Nodes, 2, "The template has changed")
+
+ req = NewRequest(t, "GET", "/user2/repo1/compare/invalid")
+ resp = session.MakeRequest(t, req, http.StatusNotFound)
+ assert.False(t, strings.Contains(resp.Body.String(), "/assets/img/500.png"), "expect 404 page not 500")
+}
+
+// Compare with inferred default branch (master)
+func TestCompareDefault(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/user2/repo1/compare/v1.1")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ selection := htmlDoc.doc.Find(".choose.branch .filter.dropdown")
+ assert.Lenf(t, selection.Nodes, 2, "The template has changed")
+}
diff --git a/tests/integration/cors_test.go b/tests/integration/cors_test.go
new file mode 100644
index 0000000000..f531801627
--- /dev/null
+++ b/tests/integration/cors_test.go
@@ -0,0 +1,23 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCORSNotSet(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestf(t, "GET", "/api/v1/version")
+ session := loginUser(t, "user2")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, resp.Code, http.StatusOK)
+ corsHeader := resp.Header().Get("Access-Control-Allow-Origin")
+ assert.Equal(t, corsHeader, "", "Access-Control-Allow-Origin: generated header should match") // header not set
+}
diff --git a/tests/integration/create_no_session_test.go b/tests/integration/create_no_session_test.go
new file mode 100644
index 0000000000..c9b90974d7
--- /dev/null
+++ b/tests/integration/create_no_session_test.go
@@ -0,0 +1,120 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers"
+ "code.gitea.io/gitea/tests"
+
+ "gitea.com/go-chi/session"
+ "github.com/stretchr/testify/assert"
+)
+
+func getSessionID(t *testing.T, resp *httptest.ResponseRecorder) string {
+ cookies := resp.Result().Cookies()
+ found := false
+ sessionID := ""
+ for _, cookie := range cookies {
+ if cookie.Name == setting.SessionConfig.CookieName {
+ sessionID = cookie.Value
+ found = true
+ }
+ }
+ assert.True(t, found)
+ assert.NotEmpty(t, sessionID)
+ return sessionID
+}
+
+func sessionFile(tmpDir, sessionID string) string {
+ return filepath.Join(tmpDir, sessionID[0:1], sessionID[1:2], sessionID)
+}
+
+func sessionFileExist(t *testing.T, tmpDir, sessionID string) bool {
+ sessionFile := sessionFile(tmpDir, sessionID)
+ _, err := os.Lstat(sessionFile)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return false
+ }
+ assert.NoError(t, err)
+ }
+ return true
+}
+
+func TestSessionFileCreation(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ oldSessionConfig := setting.SessionConfig.ProviderConfig
+ defer func() {
+ setting.SessionConfig.ProviderConfig = oldSessionConfig
+ c = routers.NormalRoutes(context.TODO())
+ }()
+
+ var config session.Options
+
+ err := json.Unmarshal([]byte(oldSessionConfig), &config)
+ assert.NoError(t, err)
+
+ config.Provider = "file"
+
+ // Now create a temporaryDirectory
+ tmpDir, err := os.MkdirTemp("", "sessions")
+ assert.NoError(t, err)
+ defer func() {
+ if _, err := os.Stat(tmpDir); !os.IsNotExist(err) {
+ _ = util.RemoveAll(tmpDir)
+ }
+ }()
+ config.ProviderConfig = tmpDir
+
+ newConfigBytes, err := json.Marshal(config)
+ assert.NoError(t, err)
+
+ setting.SessionConfig.ProviderConfig = string(newConfigBytes)
+
+ c = routers.NormalRoutes(context.TODO())
+
+ t.Run("NoSessionOnViewIssue", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/issues/1")
+ resp := MakeRequest(t, req, http.StatusOK)
+ sessionID := getSessionID(t, resp)
+
+ // We're not logged in so there should be no session
+ assert.False(t, sessionFileExist(t, tmpDir, sessionID))
+ })
+ t.Run("CreateSessionOnLogin", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/user/login")
+ resp := MakeRequest(t, req, http.StatusOK)
+ sessionID := getSessionID(t, resp)
+
+ // We're not logged in so there should be no session
+ assert.False(t, sessionFileExist(t, tmpDir, sessionID))
+
+ doc := NewHTMLParser(t, resp.Body)
+ req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
+ "_csrf": doc.GetCSRF(),
+ "user_name": "user2",
+ "password": userPassword,
+ })
+ resp = MakeRequest(t, req, http.StatusSeeOther)
+ sessionID = getSessionID(t, resp)
+
+ assert.FileExists(t, sessionFile(tmpDir, sessionID))
+ })
+}
diff --git a/tests/integration/csrf_test.go b/tests/integration/csrf_test.go
new file mode 100644
index 0000000000..18a157412b
--- /dev/null
+++ b/tests/integration/csrf_test.go
@@ -0,0 +1,53 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCsrfProtection(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // test web form csrf via form
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user.Name)
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": "fake_csrf",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ resp := session.MakeRequest(t, req, http.StatusSeeOther)
+ loc := resp.Header().Get("Location")
+ assert.Equal(t, setting.AppSubURL+"/", loc)
+ resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t, "Bad Request: invalid CSRF token",
+ strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
+ )
+
+ // test web form csrf via header. TODO: should use an UI api to test
+ req = NewRequest(t, "POST", "/user/settings")
+ req.Header.Add("X-Csrf-Token", "fake_csrf")
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ loc = resp.Header().Get("Location")
+ assert.Equal(t, setting.AppSubURL+"/", loc)
+ resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ assert.Equal(t, "Bad Request: invalid CSRF token",
+ strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
+ )
+}
diff --git a/tests/integration/delete_user_test.go b/tests/integration/delete_user_test.go
new file mode 100644
index 0000000000..1d9d257f12
--- /dev/null
+++ b/tests/integration/delete_user_test.go
@@ -0,0 +1,61 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+)
+
+func assertUserDeleted(t *testing.T, userID int64) {
+ unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
+ unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: userID})
+ unittest.AssertNotExistsBean(t, &user_model.Follow{FollowID: userID})
+ unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerID: userID})
+ unittest.AssertNotExistsBean(t, &access_model.Access{UserID: userID})
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: userID})
+ unittest.AssertNotExistsBean(t, &issues_model.IssueUser{UID: userID})
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID})
+ unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID})
+}
+
+func TestUserDeleteAccount(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user8")
+ csrf := GetCSRF(t, session, "/user/settings/account")
+ urlStr := fmt.Sprintf("/user/settings/account/delete?password=%s", userPassword)
+ req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
+ "_csrf": csrf,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ assertUserDeleted(t, 8)
+ unittest.CheckConsistencyFor(t, &user_model.User{})
+}
+
+func TestUserDeleteAccountStillOwnRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ csrf := GetCSRF(t, session, "/user/settings/account")
+ urlStr := fmt.Sprintf("/user/settings/account/delete?password=%s", userPassword)
+ req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
+ "_csrf": csrf,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // user should not have been deleted, because the user still owns repos
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+}
diff --git a/tests/integration/download_test.go b/tests/integration/download_test.go
new file mode 100644
index 0000000000..9d3b17d103
--- /dev/null
+++ b/tests/integration/download_test.go
@@ -0,0 +1,94 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDownloadByID(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ // Request raw blob
+ req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
+}
+
+func TestDownloadByIDForSVGUsesSecureHeaders(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ // Request raw blob
+ req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.HeaderMap.Get("Content-Security-Policy"))
+ assert.Equal(t, "image/svg+xml", resp.HeaderMap.Get("Content-Type"))
+ assert.Equal(t, "nosniff", resp.HeaderMap.Get("X-Content-Type-Options"))
+}
+
+func TestDownloadByIDMedia(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ // Request raw blob
+ req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
+}
+
+func TestDownloadByIDMediaForSVGUsesSecureHeaders(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ // Request raw blob
+ req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.HeaderMap.Get("Content-Security-Policy"))
+ assert.Equal(t, "image/svg+xml", resp.HeaderMap.Get("Content-Type"))
+ assert.Equal(t, "nosniff", resp.HeaderMap.Get("X-Content-Type-Options"))
+}
+
+func TestDownloadRawTextFileWithoutMimeTypeMapping(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "text/plain; charset=utf-8", resp.HeaderMap.Get("Content-Type"))
+}
+
+func TestDownloadRawTextFileWithMimeTypeMapping(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ setting.MimeTypeMap.Map[".xml"] = "text/xml"
+ setting.MimeTypeMap.Enabled = true
+
+ session := loginUser(t, "user2")
+
+ req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "text/xml; charset=utf-8", resp.HeaderMap.Get("Content-Type"))
+
+ delete(setting.MimeTypeMap.Map, ".xml")
+ setting.MimeTypeMap.Enabled = false
+}
diff --git a/tests/integration/dump_restore_test.go b/tests/integration/dump_restore_test.go
new file mode 100644
index 0000000000..19513d0271
--- /dev/null
+++ b/tests/integration/dump_restore_test.go
@@ -0,0 +1,328 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ base "code.gitea.io/gitea/modules/migration"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/migrations"
+
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/yaml.v2"
+)
+
+func TestDumpRestore(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
+ setting.Migrations.AllowLocalNetworks = true
+ AppVer := setting.AppVer
+ // Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string.
+ setting.AppVer = "1.16.0"
+ defer func() {
+ setting.Migrations.AllowLocalNetworks = AllowLocalNetworks
+ setting.AppVer = AppVer
+ }()
+
+ assert.NoError(t, migrations.Init())
+
+ reponame := "repo1"
+
+ basePath, err := os.MkdirTemp("", reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(basePath)
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ //
+ // Phase 1: dump repo1 from the Gitea instance to the filesystem
+ //
+
+ ctx := context.Background()
+ opts := migrations.MigrateOptions{
+ GitServiceType: structs.GiteaService,
+ Issues: true,
+ PullRequests: true,
+ Labels: true,
+ Milestones: true,
+ Comments: true,
+ AuthToken: token,
+ CloneAddr: repo.CloneLink().HTTPS,
+ RepoName: reponame,
+ }
+ err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
+ assert.NoError(t, err)
+
+ //
+ // Verify desired side effects of the dump
+ //
+ d := filepath.Join(basePath, repo.OwnerName, repo.Name)
+ for _, f := range []string{"repo.yml", "topic.yml", "label.yml", "milestone.yml", "issue.yml"} {
+ assert.FileExists(t, filepath.Join(d, f))
+ }
+
+ //
+ // Phase 2: restore from the filesystem to the Gitea instance in restoredrepo
+ //
+
+ newreponame := "restored"
+ err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{
+ "labels", "issues", "comments", "milestones", "pull_requests",
+ }, false)
+ assert.NoError(t, err)
+
+ newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame})
+
+ //
+ // Phase 3: dump restored from the Gitea instance to the filesystem
+ //
+ opts.RepoName = newreponame
+ opts.CloneAddr = newrepo.CloneLink().HTTPS
+ err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
+ assert.NoError(t, err)
+
+ //
+ // Verify the dump of restored is the same as the dump of repo1
+ //
+ comparator := &compareDump{
+ t: t,
+ basePath: basePath,
+ }
+ comparator.assertEquals(repo, newrepo)
+ })
+}
+
+type compareDump struct {
+ t *testing.T
+ basePath string
+ repoBefore *repo_model.Repository
+ dirBefore string
+ repoAfter *repo_model.Repository
+ dirAfter string
+}
+
+type compareField struct {
+ before interface{}
+ after interface{}
+ ignore bool
+ transform func(string) string
+ nested *compareFields
+}
+
+type compareFields map[string]compareField
+
+func (c *compareDump) replaceRepoName(original string) string {
+ return strings.ReplaceAll(original, c.repoBefore.Name, c.repoAfter.Name)
+}
+
+func (c *compareDump) assertEquals(repoBefore, repoAfter *repo_model.Repository) {
+ c.repoBefore = repoBefore
+ c.dirBefore = filepath.Join(c.basePath, repoBefore.OwnerName, repoBefore.Name)
+ c.repoAfter = repoAfter
+ c.dirAfter = filepath.Join(c.basePath, repoAfter.OwnerName, repoAfter.Name)
+
+ //
+ // base.Repository
+ //
+ _ = c.assertEqual("repo.yml", base.Repository{}, compareFields{
+ "Name": {
+ before: c.repoBefore.Name,
+ after: c.repoAfter.Name,
+ },
+ "CloneURL": {transform: c.replaceRepoName},
+ "OriginalURL": {transform: c.replaceRepoName},
+ })
+
+ //
+ // base.Label
+ //
+ labels, ok := c.assertEqual("label.yml", []base.Label{}, compareFields{}).([]*base.Label)
+ assert.True(c.t, ok)
+ assert.GreaterOrEqual(c.t, len(labels), 1)
+
+ //
+ // base.Milestone
+ //
+ milestones, ok := c.assertEqual("milestone.yml", []base.Milestone{}, compareFields{
+ "Updated": {ignore: true}, // the database updates that field independently
+ }).([]*base.Milestone)
+ assert.True(c.t, ok)
+ assert.GreaterOrEqual(c.t, len(milestones), 1)
+
+ //
+ // base.Issue and the associated comments
+ //
+ issues, ok := c.assertEqual("issue.yml", []base.Issue{}, compareFields{
+ "Assignees": {ignore: true}, // not implemented yet
+ }).([]*base.Issue)
+ assert.True(c.t, ok)
+ assert.GreaterOrEqual(c.t, len(issues), 1)
+ for _, issue := range issues {
+ filename := filepath.Join("comments", fmt.Sprintf("%d.yml", issue.Number))
+ comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{
+ "Index": {ignore: true},
+ }).([]*base.Comment)
+ assert.True(c.t, ok)
+ for _, comment := range comments {
+ assert.EqualValues(c.t, issue.Number, comment.IssueIndex)
+ }
+ }
+
+ //
+ // base.PullRequest and the associated comments
+ //
+ comparePullRequestBranch := &compareFields{
+ "RepoName": {
+ before: c.repoBefore.Name,
+ after: c.repoAfter.Name,
+ },
+ "CloneURL": {transform: c.replaceRepoName},
+ }
+ prs, ok := c.assertEqual("pull_request.yml", []base.PullRequest{}, compareFields{
+ "Assignees": {ignore: true}, // not implemented yet
+ "Head": {nested: comparePullRequestBranch},
+ "Base": {nested: comparePullRequestBranch},
+ "Labels": {ignore: true}, // because org labels are not handled properly
+ }).([]*base.PullRequest)
+ assert.True(c.t, ok)
+ assert.GreaterOrEqual(c.t, len(prs), 1)
+ for _, pr := range prs {
+ filename := filepath.Join("comments", fmt.Sprintf("%d.yml", pr.Number))
+ comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{}).([]*base.Comment)
+ assert.True(c.t, ok)
+ for _, comment := range comments {
+ assert.EqualValues(c.t, pr.Number, comment.IssueIndex)
+ }
+ }
+}
+
+func (c *compareDump) assertLoadYAMLFiles(beforeFilename, afterFilename string, before, after interface{}) {
+ _, beforeErr := os.Stat(beforeFilename)
+ _, afterErr := os.Stat(afterFilename)
+ assert.EqualValues(c.t, errors.Is(beforeErr, os.ErrNotExist), errors.Is(afterErr, os.ErrNotExist))
+ if errors.Is(beforeErr, os.ErrNotExist) {
+ return
+ }
+
+ beforeBytes, err := os.ReadFile(beforeFilename)
+ assert.NoError(c.t, err)
+ assert.NoError(c.t, yaml.Unmarshal(beforeBytes, before))
+ afterBytes, err := os.ReadFile(afterFilename)
+ assert.NoError(c.t, err)
+ assert.NoError(c.t, yaml.Unmarshal(afterBytes, after))
+}
+
+func (c *compareDump) assertLoadFiles(beforeFilename, afterFilename string, t reflect.Type) (before, after reflect.Value) {
+ var beforePtr, afterPtr reflect.Value
+ if t.Kind() == reflect.Slice {
+ //
+ // Given []Something{} create afterPtr, beforePtr []*Something{}
+ //
+ sliceType := reflect.SliceOf(reflect.PtrTo(t.Elem()))
+ beforeSlice := reflect.MakeSlice(sliceType, 0, 10)
+ beforePtr = reflect.New(beforeSlice.Type())
+ beforePtr.Elem().Set(beforeSlice)
+ afterSlice := reflect.MakeSlice(sliceType, 0, 10)
+ afterPtr = reflect.New(afterSlice.Type())
+ afterPtr.Elem().Set(afterSlice)
+ } else {
+ //
+ // Given Something{} create afterPtr, beforePtr *Something{}
+ //
+ beforePtr = reflect.New(t)
+ afterPtr = reflect.New(t)
+ }
+ c.assertLoadYAMLFiles(beforeFilename, afterFilename, beforePtr.Interface(), afterPtr.Interface())
+ return beforePtr.Elem(), afterPtr.Elem()
+}
+
+func (c *compareDump) assertEqual(filename string, kind interface{}, fields compareFields) (i interface{}) {
+ beforeFilename := filepath.Join(c.dirBefore, filename)
+ afterFilename := filepath.Join(c.dirAfter, filename)
+
+ typeOf := reflect.TypeOf(kind)
+ before, after := c.assertLoadFiles(beforeFilename, afterFilename, typeOf)
+ if typeOf.Kind() == reflect.Slice {
+ i = c.assertEqualSlices(before, after, fields)
+ } else {
+ i = c.assertEqualValues(before, after, fields)
+ }
+ return i
+}
+
+func (c *compareDump) assertEqualSlices(before, after reflect.Value, fields compareFields) interface{} {
+ assert.EqualValues(c.t, before.Len(), after.Len())
+ if before.Len() == after.Len() {
+ for i := 0; i < before.Len(); i++ {
+ _ = c.assertEqualValues(
+ reflect.Indirect(before.Index(i).Elem()),
+ reflect.Indirect(after.Index(i).Elem()),
+ fields)
+ }
+ }
+ return after.Interface()
+}
+
+func (c *compareDump) assertEqualValues(before, after reflect.Value, fields compareFields) interface{} {
+ for _, field := range reflect.VisibleFields(before.Type()) {
+ bf := before.FieldByName(field.Name)
+ bi := bf.Interface()
+ af := after.FieldByName(field.Name)
+ ai := af.Interface()
+ if compare, ok := fields[field.Name]; ok {
+ if compare.ignore == true {
+ //
+ // Ignore
+ //
+ continue
+ }
+ if compare.transform != nil {
+ //
+ // Transform these strings before comparing them
+ //
+ bs, ok := bi.(string)
+ assert.True(c.t, ok)
+ as, ok := ai.(string)
+ assert.True(c.t, ok)
+ assert.EqualValues(c.t, compare.transform(bs), compare.transform(as))
+ continue
+ }
+ if compare.before != nil && compare.after != nil {
+ //
+ // The fields are expected to have different values
+ //
+ assert.EqualValues(c.t, compare.before, bi)
+ assert.EqualValues(c.t, compare.after, ai)
+ continue
+ }
+ if compare.nested != nil {
+ //
+ // The fields are a struct, recurse
+ //
+ c.assertEqualValues(bf, af, *compare.nested)
+ continue
+ }
+ }
+ assert.EqualValues(c.t, bi, ai)
+ }
+ return after.Interface()
+}
diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go
new file mode 100644
index 0000000000..19e80dc7bf
--- /dev/null
+++ b/tests/integration/editor_test.go
@@ -0,0 +1,164 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "path"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateFile(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user2")
+
+ // Request editor page
+ req := NewRequest(t, "GET", "/user2/repo1/_new/master/")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ lastCommit := doc.GetInputValueByName("last_commit")
+ assert.NotEmpty(t, lastCommit)
+
+ // Save new file to master branch
+ req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
+ "_csrf": doc.GetCSRF(),
+ "last_commit": lastCommit,
+ "tree_path": "test.txt",
+ "content": "Content",
+ "commit_choice": "direct",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+ })
+}
+
+func TestCreateFileOnProtectedBranch(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user2")
+
+ csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
+ // Change master branch to protected
+ req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{
+ "_csrf": csrf,
+ "protected": "on",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+ // Check if master branch has been locked successfully
+ flashCookie := session.GetCookie("macaron_flash")
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527master%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
+
+ // Request editor page
+ req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ lastCommit := doc.GetInputValueByName("last_commit")
+ assert.NotEmpty(t, lastCommit)
+
+ // Save new file to master branch
+ req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
+ "_csrf": doc.GetCSRF(),
+ "last_commit": lastCommit,
+ "tree_path": "test.txt",
+ "content": "Content",
+ "commit_choice": "direct",
+ })
+
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ // Check body for error message
+ assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch &#39;master&#39;.")
+
+ // remove the protected branch
+ csrf = GetCSRF(t, session, "/user2/repo1/settings/branches")
+ // Change master branch to protected
+ req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/master", map[string]string{
+ "_csrf": csrf,
+ "protected": "off",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+ // Check if master branch has been locked successfully
+ flashCookie = session.GetCookie("macaron_flash")
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527master%2527%2Bhas%2Bbeen%2Bdisabled.", flashCookie.Value)
+ })
+}
+
+func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder {
+ // Get to the 'edit this file' page
+ req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ lastCommit := htmlDoc.GetInputValueByName("last_commit")
+ assert.NotEmpty(t, lastCommit)
+
+ // Submit the edits
+ req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
+ map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "last_commit": lastCommit,
+ "tree_path": filePath,
+ "content": newContent,
+ "commit_choice": "direct",
+ },
+ )
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // Verify the change
+ req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.EqualValues(t, newContent, resp.Body.String())
+
+ return resp
+}
+
+func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder {
+ // Get to the 'edit this file' page
+ req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ lastCommit := htmlDoc.GetInputValueByName("last_commit")
+ assert.NotEmpty(t, lastCommit)
+
+ // Submit the edits
+ req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
+ map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "last_commit": lastCommit,
+ "tree_path": filePath,
+ "content": newContent,
+ "commit_choice": "commit-to-new-branch",
+ "new_branch_name": targetBranch,
+ },
+ )
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // Verify the change
+ req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.EqualValues(t, newContent, resp.Body.String())
+
+ return resp
+}
+
+func TestEditFile(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user2")
+ testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+ })
+}
+
+func TestEditFileToNewBranch(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user2")
+ testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
+ })
+}
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
new file mode 100644
index 0000000000..8810363dc8
--- /dev/null
+++ b/tests/integration/empty_repo_test.go
@@ -0,0 +1,31 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestEmptyRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ subpaths := []string{
+ "commits/master",
+ "raw/foo",
+ "commit/1ae57b34ccf7e18373",
+ "graph",
+ }
+ emptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{}, unittest.Cond("is_empty = ?", true))
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: emptyRepo.OwnerID})
+ for _, subpath := range subpaths {
+ req := NewRequestf(t, "GET", "/%s/%s/%s", owner.Name, emptyRepo.Name, subpath)
+ MakeRequest(t, req, http.StatusNotFound)
+ }
+}
diff --git a/tests/integration/eventsource_test.go b/tests/integration/eventsource_test.go
new file mode 100644
index 0000000000..cd496e0129
--- /dev/null
+++ b/tests/integration/eventsource_test.go
@@ -0,0 +1,83 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/eventsource"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEventSourceManagerRun(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ manager := eventsource.GetManager()
+
+ eventChan := manager.Register(2)
+ defer func() {
+ manager.Unregister(2, eventChan)
+ // ensure the eventChan is closed
+ for {
+ _, ok := <-eventChan
+ if !ok {
+ break
+ }
+ }
+ }()
+ expectNotificationCountEvent := func(count int64) func() bool {
+ return func() bool {
+ select {
+ case event, ok := <-eventChan:
+ if !ok {
+ return false
+ }
+ data, ok := event.Data.(activities_model.UserIDCount)
+ if !ok {
+ return false
+ }
+ return event.Name == "notification-count" && data.Count == count
+ default:
+ return false
+ }
+ }
+ }
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
+ assert.NoError(t, thread5.LoadAttributes())
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ var apiNL []api.NotificationThread
+
+ // -- mark notifications as read --
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &apiNL)
+ assert.Len(t, apiNL, 2)
+
+ lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 <- only Notification 4 is in this filter ...
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token))
+ session.MakeRequest(t, req, http.StatusResetContent)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s&status-types=unread", token))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiNL)
+ assert.Len(t, apiNL, 1)
+
+ assert.Eventually(t, expectNotificationCountEvent(1), 30*time.Second, 1*time.Second)
+}
diff --git a/tests/integration/explore_repos_test.go b/tests/integration/explore_repos_test.go
new file mode 100644
index 0000000000..dca3252753
--- /dev/null
+++ b/tests/integration/explore_repos_test.go
@@ -0,0 +1,19 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+)
+
+func TestExploreRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/explore/repos")
+ MakeRequest(t, req, http.StatusOK)
+}
diff --git a/tests/integration/git_clone_wiki_test.go b/tests/integration/git_clone_wiki_test.go
new file mode 100644
index 0000000000..4bdbc9b7c3
--- /dev/null
+++ b/tests/integration/git_clone_wiki_test.go
@@ -0,0 +1,53 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func assertFileExist(t *testing.T, p string) {
+ exist, err := util.IsExist(p)
+ assert.NoError(t, err)
+ assert.True(t, exist)
+}
+
+func assertFileEqual(t *testing.T, p string, content []byte) {
+ bs, err := os.ReadFile(p)
+ assert.NoError(t, err)
+ assert.EqualValues(t, content, bs)
+}
+
+func TestRepoCloneWiki(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ defer tests.PrepareTestEnv(t)()
+
+ dstPath, err := os.MkdirTemp("", "clone_wiki")
+ assert.NoError(t, err)
+
+ r := fmt.Sprintf("%suser2/repo1.wiki.git", u.String())
+ u, _ = url.Parse(r)
+ u.User = url.UserPassword("user2", userPassword)
+ t.Run("Clone", func(t *testing.T) {
+ assert.NoError(t, git.CloneWithArgs(context.Background(), u.String(), dstPath, git.AllowLFSFiltersArgs(), git.CloneRepoOptions{}))
+ assertFileEqual(t, filepath.Join(dstPath, "Home.md"), []byte("# Home page\n\nThis is the home page!\n"))
+ assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md"))
+ assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md"))
+ assertFileExist(t, filepath.Join(dstPath, "images"))
+ assertFileExist(t, filepath.Join(dstPath, "jpeg.jpg"))
+ })
+ })
+}
diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go
new file mode 100644
index 0000000000..666f9f6fe9
--- /dev/null
+++ b/tests/integration/git_helper_for_declarative_test.go
@@ -0,0 +1,202 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/ssh"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func withKeyFile(t *testing.T, keyname string, callback func(string)) {
+ tmpDir, err := os.MkdirTemp("", "key-file")
+ assert.NoError(t, err)
+ defer util.RemoveAll(tmpDir)
+
+ err = os.Chmod(tmpDir, 0o700)
+ assert.NoError(t, err)
+
+ keyFile := filepath.Join(tmpDir, keyname)
+ err = ssh.GenKeyPair(keyFile)
+ assert.NoError(t, err)
+
+ err = os.WriteFile(path.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+
+ "ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0o700)
+ assert.NoError(t, err)
+
+ // Setup ssh wrapper
+ os.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
+ os.Setenv("GIT_SSH_COMMAND",
+ "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i \""+keyFile+"\"")
+ os.Setenv("GIT_SSH_VARIANT", "ssh")
+
+ callback(keyFile)
+}
+
+func createSSHUrl(gitPath string, u *url.URL) *url.URL {
+ u2 := *u
+ u2.Scheme = "ssh"
+ u2.User = url.User("git")
+ u2.Host = net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort))
+ u2.Path = gitPath
+ return &u2
+}
+
+func onGiteaRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
+ if len(prepare) == 0 || prepare[0] {
+ defer tests.PrepareTestEnv(t, 1)()
+ }
+ s := http.Server{
+ Handler: c,
+ }
+
+ u, err := url.Parse(setting.AppURL)
+ assert.NoError(t, err)
+ listener, err := net.Listen("tcp", u.Host)
+ i := 0
+ for err != nil && i <= 10 {
+ time.Sleep(100 * time.Millisecond)
+ listener, err = net.Listen("tcp", u.Host)
+ i++
+ }
+ assert.NoError(t, err)
+ u.Host = listener.Addr().String()
+
+ defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ s.Shutdown(ctx)
+ cancel()
+ }()
+
+ go s.Serve(listener)
+ // Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
+
+ callback(t, u)
+}
+
+func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...bool) {
+ onGiteaRunTB(t, func(t testing.TB, u *url.URL) {
+ callback(t.(*testing.T), u)
+ }, prepare...)
+}
+
+func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) {
+ return func(t *testing.T) {
+ assert.NoError(t, git.CloneWithArgs(context.Background(), u.String(), dstLocalPath, git.AllowLFSFiltersArgs(), git.CloneRepoOptions{}))
+ exist, err := util.IsExist(filepath.Join(dstLocalPath, "README.md"))
+ assert.NoError(t, err)
+ assert.True(t, exist)
+ }
+}
+
+func doPartialGitClone(dstLocalPath string, u *url.URL) func(*testing.T) {
+ return func(t *testing.T) {
+ assert.NoError(t, git.CloneWithArgs(context.Background(), u.String(), dstLocalPath, git.AllowLFSFiltersArgs(), git.CloneRepoOptions{
+ Filter: "blob:none",
+ }))
+ exist, err := util.IsExist(filepath.Join(dstLocalPath, "README.md"))
+ assert.NoError(t, err)
+ assert.True(t, exist)
+ }
+}
+
+func doGitCloneFail(u *url.URL) func(*testing.T) {
+ return func(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "doGitCloneFail")
+ assert.NoError(t, err)
+ defer util.RemoveAll(tmpDir)
+ assert.Error(t, git.Clone(git.DefaultContext, u.String(), tmpDir, git.CloneRepoOptions{}))
+ exist, err := util.IsExist(filepath.Join(tmpDir, "README.md"))
+ assert.NoError(t, err)
+ assert.False(t, exist)
+ }
+}
+
+func doGitInitTestRepository(dstPath string) func(*testing.T) {
+ return func(t *testing.T) {
+ // Init repository in dstPath
+ assert.NoError(t, git.InitRepository(git.DefaultContext, dstPath, false))
+ // forcibly set default branch to master
+ _, _, err := git.NewCommand(git.DefaultContext, "symbolic-ref", "HEAD", git.BranchPrefix+"master").RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ assert.NoError(t, os.WriteFile(filepath.Join(dstPath, "README.md"), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s", dstPath)), 0o644))
+ assert.NoError(t, git.AddChanges(dstPath, true))
+ signature := git.Signature{
+ Email: "test@example.com",
+ Name: "test",
+ When: time.Now(),
+ }
+ assert.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{
+ Committer: &signature,
+ Author: &signature,
+ Message: "Initial Commit",
+ }))
+ }
+}
+
+func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) {
+ return func(t *testing.T) {
+ _, _, err := git.NewCommand(git.DefaultContext, "remote", "add", remoteName, u.String()).RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ }
+}
+
+func doGitPushTestRepository(dstPath string, args ...string) func(*testing.T) {
+ return func(t *testing.T) {
+ _, _, err := git.NewCommand(git.DefaultContext, append([]string{"push", "-u"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ }
+}
+
+func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T) {
+ return func(t *testing.T) {
+ _, _, err := git.NewCommand(git.DefaultContext, append([]string{"push"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.Error(t, err)
+ }
+}
+
+func doGitCreateBranch(dstPath, branch string) func(*testing.T) {
+ return func(t *testing.T) {
+ _, _, err := git.NewCommand(git.DefaultContext, "checkout", "-b", branch).RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ }
+}
+
+func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) {
+ return func(t *testing.T) {
+ _, _, err := git.NewCommandNoGlobals(append(append(git.AllowLFSFiltersArgs(), "checkout"), args...)...).RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ }
+}
+
+func doGitMerge(dstPath string, args ...string) func(*testing.T) {
+ return func(t *testing.T) {
+ _, _, err := git.NewCommand(git.DefaultContext, append([]string{"merge"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ }
+}
+
+func doGitPull(dstPath string, args ...string) func(*testing.T) {
+ return func(t *testing.T) {
+ _, _, err := git.NewCommandNoGlobals(append(append(git.AllowLFSFiltersArgs(), "pull"), args...)...).RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ }
+}
diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go
new file mode 100644
index 0000000000..02b0e93870
--- /dev/null
+++ b/tests/integration/git_smart_http_test.go
@@ -0,0 +1,69 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "io"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGitSmartHTTP(t *testing.T) {
+ onGiteaRun(t, testGitSmartHTTP)
+}
+
+func testGitSmartHTTP(t *testing.T, u *url.URL) {
+ kases := []struct {
+ p string
+ code int
+ }{
+ {
+ p: "user2/repo1/info/refs",
+ code: http.StatusOK,
+ },
+ {
+ p: "user2/repo1/HEAD",
+ code: http.StatusOK,
+ },
+ {
+ p: "user2/repo1/objects/info/alternates",
+ code: http.StatusNotFound,
+ },
+ {
+ p: "user2/repo1/objects/info/http-alternates",
+ code: http.StatusNotFound,
+ },
+ {
+ p: "user2/repo1/../../custom/conf/app.ini",
+ code: http.StatusNotFound,
+ },
+ {
+ p: "user2/repo1/objects/info/../../../../custom/conf/app.ini",
+ code: http.StatusNotFound,
+ },
+ {
+ p: `user2/repo1/objects/info/..\..\..\..\custom\conf\app.ini`,
+ code: http.StatusBadRequest,
+ },
+ }
+
+ for _, kase := range kases {
+ t.Run(kase.p, func(t *testing.T) {
+ p := u.String() + kase.p
+ req, err := http.NewRequest("GET", p, nil)
+ assert.NoError(t, err)
+ req.SetBasicAuth("user2", userPassword)
+ resp, err := http.DefaultClient.Do(req)
+ assert.NoError(t, err)
+ defer resp.Body.Close()
+ assert.EqualValues(t, kase.code, resp.StatusCode)
+ _, err = io.ReadAll(resp.Body)
+ assert.NoError(t, err)
+ })
+ }
+}
diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go
new file mode 100644
index 0000000000..caeb5db8b3
--- /dev/null
+++ b/tests/integration/git_test.go
@@ -0,0 +1,851 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "encoding/hex"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ littleSize = 1024 // 1ko
+ bigSize = 128 * 1024 * 1024 // 128Mo
+)
+
+func TestGit(t *testing.T) {
+ onGiteaRun(t, testGit)
+}
+
+func testGit(t *testing.T, u *url.URL) {
+ username := "user2"
+ baseAPITestContext := NewAPITestContext(t, username, "repo1")
+
+ u.Path = baseAPITestContext.GitPath()
+
+ forkedUserCtx := NewAPITestContext(t, "user4", "repo1")
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ ensureAnonymousClone(t, u)
+ httpContext := baseAPITestContext
+ httpContext.Reponame = "repo-tmp-17"
+ forkedUserCtx.Reponame = httpContext.Reponame
+
+ dstPath, err := os.MkdirTemp("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
+ t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, httpContext.Username, perm.AccessModeRead))
+
+ t.Run("ForkFromDifferentUser", doAPIForkRepository(httpContext, forkedUserCtx.Username))
+
+ u.Path = httpContext.GitPath()
+ u.User = url.UserPassword(username, userPassword)
+
+ t.Run("Clone", doGitClone(dstPath, u))
+
+ dstPath2, err := os.MkdirTemp("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath2)
+
+ t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
+
+ little, big := standardCommitAndPushTest(t, dstPath)
+ littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
+ rawTest(t, &httpContext, little, big, littleLFS, bigLFS)
+ mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)
+
+ t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head"))
+ t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
+ t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath))
+ t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
+ t.Run("MergeFork", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master"))
+ rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
+ mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
+ })
+
+ t.Run("PushCreate", doPushCreate(httpContext, u))
+ })
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ sshContext := baseAPITestContext
+ sshContext.Reponame = "repo-tmp-18"
+ keyname := "my-testing-key"
+ forkedUserCtx.Reponame = sshContext.Reponame
+ t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
+ t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, sshContext.Username, perm.AccessModeRead))
+ t.Run("ForkFromDifferentUser", doAPIForkRepository(sshContext, forkedUserCtx.Username))
+
+ // Setup key the user ssh key
+ withKeyFile(t, keyname, func(keyFile string) {
+ t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile))
+
+ // Setup remote link
+ // TODO: get url from api
+ sshURL := createSSHUrl(sshContext.GitPath(), u)
+
+ // Setup clone folder
+ dstPath, err := os.MkdirTemp("", sshContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ t.Run("Clone", doGitClone(dstPath, sshURL))
+
+ little, big := standardCommitAndPushTest(t, dstPath)
+ littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
+ rawTest(t, &sshContext, little, big, littleLFS, bigLFS)
+ mediaTest(t, &sshContext, little, big, littleLFS, bigLFS)
+
+ t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "master", "test/head2"))
+ t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
+ t.Run("MergeFork", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master"))
+ rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
+ mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
+ })
+
+ t.Run("PushCreate", doPushCreate(sshContext, sshURL))
+ })
+ })
+}
+
+func ensureAnonymousClone(t *testing.T, u *url.URL) {
+ dstLocalPath, err := os.MkdirTemp("", "repo1")
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstLocalPath)
+ t.Run("CloneAnonymous", doGitClone(dstLocalPath, u))
+}
+
+func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string) {
+ t.Run("Standard", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ little, big = commitAndPushTest(t, dstPath, "data-file-")
+ })
+ return little, big
+}
+
+func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) {
+ t.Run("LFS", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ prefix := "lfs-data-file-"
+ err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("install").Run(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("track", prefix+"*").RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+ err = git.AddChanges(dstPath, false, ".gitattributes")
+ assert.NoError(t, err)
+
+ err = git.CommitChangesWithArgs(dstPath, git.AllowLFSFiltersArgs(), git.CommitChangesOptions{
+ Committer: &git.Signature{
+ Email: "user2@example.com",
+ Name: "User Two",
+ When: time.Now(),
+ },
+ Author: &git.Signature{
+ Email: "user2@example.com",
+ Name: "User Two",
+ When: time.Now(),
+ },
+ Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
+ })
+ assert.NoError(t, err)
+
+ littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix)
+
+ t.Run("Locks", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ lockTest(t, dstPath)
+ })
+ })
+ return littleLFS, bigLFS
+}
+
+func commitAndPushTest(t *testing.T, dstPath, prefix string) (little, big string) {
+ t.Run("PushCommit", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ t.Run("Little", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ little = doCommitAndPush(t, littleSize, dstPath, prefix)
+ })
+ t.Run("Big", func(t *testing.T) {
+ if testing.Short() {
+ t.Skip("Skipping test in short mode.")
+ return
+ }
+ defer tests.PrintCurrentTest(t)()
+ big = doCommitAndPush(t, bigSize, dstPath, prefix)
+ })
+ })
+ return little, big
+}
+
+func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
+ t.Run("Raw", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ username := ctx.Username
+ reponame := ctx.Reponame
+
+ session := loginUser(t, username)
+
+ // Request raw paths
+ req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little))
+ resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+ assert.Equal(t, littleSize, resp.Length)
+
+ if setting.LFS.StartServer {
+ req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEqual(t, littleSize, resp.Body.Len())
+ assert.LessOrEqual(t, resp.Body.Len(), 1024)
+ if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 {
+ assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
+ }
+ }
+
+ if !testing.Short() {
+ req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
+ resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+ assert.Equal(t, bigSize, resp.Length)
+
+ if setting.LFS.StartServer {
+ req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ assert.NotEqual(t, bigSize, resp.Body.Len())
+ if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 {
+ assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
+ }
+ }
+ }
+ })
+}
+
+func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
+ t.Run("Media", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ username := ctx.Username
+ reponame := ctx.Reponame
+
+ session := loginUser(t, username)
+
+ // Request media paths
+ req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little))
+ resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+ assert.Equal(t, littleSize, resp.Length)
+
+ req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
+ resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+ assert.Equal(t, littleSize, resp.Length)
+
+ if !testing.Short() {
+ req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
+ resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+ assert.Equal(t, bigSize, resp.Length)
+
+ if setting.LFS.StartServer {
+ req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
+ resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+ assert.Equal(t, bigSize, resp.Length)
+ }
+ }
+ })
+}
+
+func lockTest(t *testing.T, repoPath string) {
+ lockFileTest(t, "README.md", repoPath)
+}
+
+func lockFileTest(t *testing.T, filename, repoPath string) {
+ _, _, err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath})
+ assert.NoError(t, err)
+ _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("lock", filename).RunStdString(&git.RunOpts{Dir: repoPath})
+ assert.NoError(t, err)
+ _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath})
+ assert.NoError(t, err)
+ _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("unlock", filename).RunStdString(&git.RunOpts{Dir: repoPath})
+ assert.NoError(t, err)
+}
+
+func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string {
+ name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix)
+ assert.NoError(t, err)
+ _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "master").RunStdString(&git.RunOpts{Dir: repoPath}) // Push
+ assert.NoError(t, err)
+ return name
+}
+
+func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) {
+ // Generate random file
+ bufSize := 4 * 1024
+ if bufSize > size {
+ bufSize = size
+ }
+
+ buffer := make([]byte, bufSize)
+
+ tmpFile, err := os.CreateTemp(repoPath, prefix)
+ if err != nil {
+ return "", err
+ }
+ defer tmpFile.Close()
+ written := 0
+ for written < size {
+ n := size - written
+ if n > bufSize {
+ n = bufSize
+ }
+ _, err := rand.Read(buffer[:n])
+ if err != nil {
+ return "", err
+ }
+ n, err = tmpFile.Write(buffer[:n])
+ if err != nil {
+ return "", err
+ }
+ written += n
+ }
+ if err != nil {
+ return "", err
+ }
+
+ // Commit
+ // Now here we should explicitly allow lfs filters to run
+ globalArgs := git.AllowLFSFiltersArgs()
+ err = git.AddChangesWithArgs(repoPath, globalArgs, false, filepath.Base(tmpFile.Name()))
+ if err != nil {
+ return "", err
+ }
+ err = git.CommitChangesWithArgs(repoPath, globalArgs, git.CommitChangesOptions{
+ Committer: &git.Signature{
+ Email: email,
+ Name: fullName,
+ When: time.Now(),
+ },
+ Author: &git.Signature{
+ Email: email,
+ Name: fullName,
+ When: time.Now(),
+ },
+ Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
+ })
+ return filepath.Base(tmpFile.Name()), err
+}
+
+func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected"))
+ t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
+
+ ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame)
+ t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", ""))
+ t.Run("GenerateCommit", func(t *testing.T) {
+ _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ assert.NoError(t, err)
+ })
+ t.Run("FailToPushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "origin", "protected"))
+ t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
+ var pr api.PullRequest
+ var err error
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t)
+ assert.NoError(t, err)
+ })
+ t.Run("GenerateCommit", func(t *testing.T) {
+ _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ assert.NoError(t, err)
+ })
+ t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2"))
+ var pr2 api.PullRequest
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t)
+ assert.NoError(t, err)
+ })
+ t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index))
+ t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+ t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
+
+ t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "unprotected-file-*"))
+ t.Run("GenerateCommit", func(t *testing.T) {
+ _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-")
+ assert.NoError(t, err)
+ })
+ t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
+
+ t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, ""))
+
+ t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
+ t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
+ t.Run("GenerateCommit", func(t *testing.T) {
+ _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ assert.NoError(t, err)
+ })
+ t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected"))
+ t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected"))
+ t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected"))
+ t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
+ }
+}
+
+func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFilePatterns string) func(t *testing.T) {
+ // We are going to just use the owner to set the protection.
+ return func(t *testing.T) {
+ csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
+
+ if userToWhitelist == "" {
+ // Change branch to protected
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
+ "_csrf": csrf,
+ "protected": "on",
+ "unprotected_file_patterns": unprotectedFilePatterns,
+ })
+ ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
+ } else {
+ user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelist)
+ assert.NoError(t, err)
+ // Change branch to protected
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
+ "_csrf": csrf,
+ "protected": "on",
+ "enable_push": "whitelist",
+ "enable_whitelist": "on",
+ "whitelist_users": strconv.FormatInt(user.ID, 10),
+ "unprotected_file_patterns": unprotectedFilePatterns,
+ })
+ ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
+ }
+ // Check if master branch has been locked successfully
+ flashCookie := ctx.Session.GetCookie("macaron_flash")
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
+ }
+}
+
+func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ var pr api.PullRequest
+ var err error
+
+ // Create a test pullrequest
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
+ assert.NoError(t, err)
+ })
+
+ // Ensure the PR page works
+ t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+
+ // Then get the diff string
+ var diffHash string
+ var diffLength int
+ t.Run("GetDiff", func(t *testing.T) {
+ req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(baseCtx.Username), url.PathEscape(baseCtx.Reponame), pr.Index))
+ resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
+ diffHash = string(resp.Hash.Sum(nil))
+ diffLength = resp.Length
+ })
+
+ // Now: Merge the PR & make sure that doesn't break the PR page or change its diff
+ t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+ t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+ t.Run("CheckPR", func(t *testing.T) {
+ oldMergeBase := pr.MergeBase
+ pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+ assert.NoError(t, err)
+ assert.Equal(t, oldMergeBase, pr2.MergeBase)
+ })
+ t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
+
+ // Then: Delete the head branch & make sure that doesn't break the PR page or change its diff
+ t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch))
+ t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+ t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
+
+ // Delete the head repository & make sure that doesn't break the PR page or change its diff
+ t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx))
+ t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+ t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
+ }
+}
+
+func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ var (
+ pr api.PullRequest
+ err error
+ lastCommitID string
+ )
+
+ trueBool := true
+ falseBool := false
+
+ t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{
+ HasPullRequests: &trueBool,
+ AllowManualMerge: &trueBool,
+ AutodetectManualMerge: &falseBool,
+ }))
+
+ t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
+ t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch))
+ t.Run("CreateEmptyPullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
+ assert.NoError(t, err)
+ })
+ lastCommitID = pr.Base.Sha
+ t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index))
+ }
+}
+
+func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) {
+ return func(t *testing.T) {
+ req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+ ctx.Session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+ ctx.Session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+ ctx.Session.MakeRequest(t, req, http.StatusOK)
+ }
+}
+
+func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffHash string, diffLength int) func(t *testing.T) {
+ return func(t *testing.T) {
+ req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+ resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
+ actual := string(resp.Hash.Sum(nil))
+ actualLength := resp.Length
+
+ equal := diffHash == actual
+ assert.True(t, equal, "Unexpected change in the diff string: expected hash: %s size: %d but was actually: %s size: %d", hex.EncodeToString([]byte(diffHash)), diffLength, hex.EncodeToString([]byte(actual)), actualLength)
+ }
+}
+
+func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // create a context for a currently non-existent repository
+ ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme)
+ u.Path = ctx.GitPath()
+
+ // Create a temporary directory
+ tmpDir, err := os.MkdirTemp("", ctx.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(tmpDir)
+
+ // Now create local repository to push as our test and set its origin
+ t.Run("InitTestRepository", doGitInitTestRepository(tmpDir))
+ t.Run("AddRemote", doGitAddRemote(tmpDir, "origin", u))
+
+ // Disable "Push To Create" and attempt to push
+ setting.Repository.EnablePushCreateUser = false
+ t.Run("FailToPushAndCreateTestRepository", doGitPushTestRepositoryFail(tmpDir, "origin", "master"))
+
+ // Enable "Push To Create"
+ setting.Repository.EnablePushCreateUser = true
+
+ // Assert that cloning from a non-existent repository does not create it and that it definitely wasn't create above
+ t.Run("FailToCloneFromNonExistentRepository", doGitCloneFail(u))
+
+ // Then "Push To Create"x
+ t.Run("SuccessfullyPushAndCreateTestRepository", doGitPushTestRepository(tmpDir, "origin", "master"))
+
+ // Finally, fetch repo from database and ensure the correct repository has been created
+ repo, err := repo_model.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
+ assert.NoError(t, err)
+ assert.False(t, repo.IsEmpty)
+ assert.True(t, repo.IsPrivate)
+
+ // Now add a remote that is invalid to "Push To Create"
+ invalidCtx := ctx
+ invalidCtx.Reponame = fmt.Sprintf("invalid/repo-tmp-push-create-%s", u.Scheme)
+ u.Path = invalidCtx.GitPath()
+ t.Run("AddInvalidRemote", doGitAddRemote(tmpDir, "invalid", u))
+
+ // Fail to "Push To Create" the invalid
+ t.Run("FailToPushAndCreateInvalidTestRepository", doGitPushTestRepositoryFail(tmpDir, "invalid", "master"))
+ }
+}
+
+func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) {
+ return func(t *testing.T) {
+ csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/branches", url.PathEscape(owner), url.PathEscape(repo)))
+
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{
+ "_csrf": csrf,
+ })
+ ctx.Session.MakeRequest(t, req, http.StatusOK)
+ }
+}
+
+func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame)
+
+ t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
+ t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
+ t.Run("GenerateCommit", func(t *testing.T) {
+ _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+ assert.NoError(t, err)
+ })
+ t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3"))
+ var pr api.PullRequest
+ var err error
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected3")(t)
+ assert.NoError(t, err)
+ })
+
+ // Request repository commits page
+ req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", baseCtx.Username, baseCtx.Reponame, pr.Index))
+ resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+
+ // Get first commit URL
+ commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
+ assert.True(t, exists)
+ assert.NotEmpty(t, commitURL)
+
+ commitID := path.Base(commitURL)
+
+ // Call API to add Pending status for commit
+ t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusPending))
+
+ // Cancel not existing auto merge
+ ctx.ExpectedCode = http.StatusNotFound
+ t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+ // Add auto merge request
+ ctx.ExpectedCode = http.StatusCreated
+ t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+ // Can not create schedule twice
+ ctx.ExpectedCode = http.StatusConflict
+ t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+ // Cancel auto merge request
+ ctx.ExpectedCode = http.StatusNoContent
+ t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+ // Add auto merge request
+ ctx.ExpectedCode = http.StatusCreated
+ t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+ // Check pr status
+ ctx.ExpectedCode = 0
+ pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+ assert.NoError(t, err)
+ assert.False(t, pr.HasMerged)
+
+ // Call API to add Failure status for commit
+ t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusFailure))
+
+ // Check pr status
+ pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+ assert.NoError(t, err)
+ assert.False(t, pr.HasMerged)
+
+ // Call API to add Success status for commit
+ t.Run("CreateStatus", doAPICreateCommitStatus(ctx, commitID, api.CommitStatusSuccess))
+
+ // wait to let gitea merge stuff
+ time.Sleep(time.Second)
+
+ // test pr status
+ pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+ assert.NoError(t, err)
+ assert.True(t, pr.HasMerged)
+ }
+}
+
+func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) {
+ return func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // skip this test if git version is low
+ if git.CheckGitVersionAtLeast("2.29") != nil {
+ return
+ }
+
+ gitRepo, err := git.OpenRepository(git.DefaultContext, dstPath)
+ if !assert.NoError(t, err) {
+ return
+ }
+ defer gitRepo.Close()
+
+ var (
+ pr1, pr2 *issues_model.PullRequest
+ commit string
+ )
+ repo, err := repo_model.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
+ if !assert.NoError(t, err) {
+ return
+ }
+
+ pullNum := unittest.GetCount(t, &issues_model.PullRequest{})
+
+ t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
+
+ t.Run("AddCommit", func(t *testing.T) {
+ err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
+ if !assert.NoError(t, err) {
+ return
+ }
+
+ err = git.AddChanges(dstPath, true)
+ assert.NoError(t, err)
+
+ err = git.CommitChanges(dstPath, git.CommitChangesOptions{
+ Committer: &git.Signature{
+ Email: "user2@example.com",
+ Name: "user2",
+ When: time.Now(),
+ },
+ Author: &git.Signature{
+ Email: "user2@example.com",
+ Name: "user2",
+ When: time.Now(),
+ },
+ Message: "Testing commit 1",
+ })
+ assert.NoError(t, err)
+ commit, err = gitRepo.GetRefCommitID("HEAD")
+ assert.NoError(t, err)
+ })
+
+ t.Run("Push", func(t *testing.T) {
+ err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).Run(&git.RunOpts{Dir: dstPath})
+ if !assert.NoError(t, err) {
+ return
+ }
+ unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1)
+ pr1 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+ HeadRepoID: repo.ID,
+ Flow: issues_model.PullRequestFlowAGit,
+ })
+ if !assert.NotEmpty(t, pr1) {
+ return
+ }
+ prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
+ if !assert.NoError(t, err) {
+ return
+ }
+ assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch)
+ assert.Equal(t, false, prMsg.HasMerged)
+ assert.Contains(t, "Testing commit 1", prMsg.Body)
+ assert.Equal(t, commit, prMsg.Head.Sha)
+
+ _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunStdString(&git.RunOpts{Dir: dstPath})
+ if !assert.NoError(t, err) {
+ return
+ }
+ unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
+ pr2 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+ HeadRepoID: repo.ID,
+ Index: pr1.Index + 1,
+ Flow: issues_model.PullRequestFlowAGit,
+ })
+ if !assert.NotEmpty(t, pr2) {
+ return
+ }
+ prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
+ if !assert.NoError(t, err) {
+ return
+ }
+ assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch)
+ assert.Equal(t, false, prMsg.HasMerged)
+ })
+
+ if pr1 == nil || pr2 == nil {
+ return
+ }
+
+ t.Run("AddCommit2", func(t *testing.T) {
+ err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666)
+ if !assert.NoError(t, err) {
+ return
+ }
+
+ err = git.AddChanges(dstPath, true)
+ assert.NoError(t, err)
+
+ err = git.CommitChanges(dstPath, git.CommitChangesOptions{
+ Committer: &git.Signature{
+ Email: "user2@example.com",
+ Name: "user2",
+ When: time.Now(),
+ },
+ Author: &git.Signature{
+ Email: "user2@example.com",
+ Name: "user2",
+ When: time.Now(),
+ },
+ Message: "Testing commit 2",
+ })
+ assert.NoError(t, err)
+ commit, err = gitRepo.GetRefCommitID("HEAD")
+ assert.NoError(t, err)
+ })
+
+ t.Run("Push2", func(t *testing.T) {
+ err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).Run(&git.RunOpts{Dir: dstPath})
+ if !assert.NoError(t, err) {
+ return
+ }
+ unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
+ prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
+ if !assert.NoError(t, err) {
+ return
+ }
+ assert.Equal(t, false, prMsg.HasMerged)
+ assert.Equal(t, commit, prMsg.Head.Sha)
+
+ _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunStdString(&git.RunOpts{Dir: dstPath})
+ if !assert.NoError(t, err) {
+ return
+ }
+ unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
+ prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
+ if !assert.NoError(t, err) {
+ return
+ }
+ assert.Equal(t, false, prMsg.HasMerged)
+ assert.Equal(t, commit, prMsg.Head.Sha)
+ })
+ t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index))
+ t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
+ }
+}
diff --git a/tests/integration/goget_test.go b/tests/integration/goget_test.go
new file mode 100644
index 0000000000..c969f4aff1
--- /dev/null
+++ b/tests/integration/goget_test.go
@@ -0,0 +1,36 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGoGet(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/blah/glah/plah?go-get=1")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ expected := fmt.Sprintf(`<!doctype html>
+<html>
+ <head>
+ <meta name="go-import" content="%[1]s:%[2]s/blah/glah git %[3]sblah/glah.git">
+ <meta name="go-source" content="%[1]s:%[2]s/blah/glah _ %[3]sblah/glah/src/branch/master{/dir} %[3]sblah/glah/src/branch/master{/dir}/{file}#L{line}">
+ </head>
+ <body>
+ go get --insecure %[1]s:%[2]s/blah/glah
+ </body>
+</html>`, setting.Domain, setting.HTTPPort, setting.AppURL)
+
+ assert.Equal(t, expected, resp.Body.String())
+}
diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go
new file mode 100644
index 0000000000..2e16d150c8
--- /dev/null
+++ b/tests/integration/gpg_git_test.go
@@ -0,0 +1,369 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "os"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/crypto/openpgp/armor"
+)
+
+func TestGPGGit(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ username := "user2"
+
+ // OK Set a new GPG home
+ tmpDir, err := os.MkdirTemp("", "temp-gpg")
+ assert.NoError(t, err)
+ defer util.RemoveAll(tmpDir)
+
+ err = os.Chmod(tmpDir, 0o700)
+ assert.NoError(t, err)
+
+ oldGNUPGHome := os.Getenv("GNUPGHOME")
+ err = os.Setenv("GNUPGHOME", tmpDir)
+ assert.NoError(t, err)
+ defer os.Setenv("GNUPGHOME", oldGNUPGHome)
+
+ // Need to create a root key
+ rootKeyPair, err := importTestingKey(tmpDir, "gitea", "gitea@fake.local")
+ assert.NoError(t, err)
+ if err != nil {
+ assert.FailNow(t, "Unable to import rootKeyPair")
+ }
+
+ rootKeyID := rootKeyPair.PrimaryKey.KeyIdShortString()
+
+ oldKeyID := setting.Repository.Signing.SigningKey
+ oldName := setting.Repository.Signing.SigningName
+ oldEmail := setting.Repository.Signing.SigningEmail
+ defer func() {
+ setting.Repository.Signing.SigningKey = oldKeyID
+ setting.Repository.Signing.SigningName = oldName
+ setting.Repository.Signing.SigningEmail = oldEmail
+ }()
+
+ setting.Repository.Signing.SigningKey = rootKeyID
+ setting.Repository.Signing.SigningName = "gitea"
+ setting.Repository.Signing.SigningEmail = "gitea@fake.local"
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
+
+ setting.Repository.Signing.InitialCommit = []string{"never"}
+ setting.Repository.Signing.CRUDActions = []string{"never"}
+
+ baseAPITestContext := NewAPITestContext(t, username, "repo1")
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("Unsigned-Initial", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
+ t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
+ assert.NotNil(t, branch.Commit)
+ assert.NotNil(t, branch.Commit.Verification)
+ assert.False(t, branch.Commit.Verification.Verified)
+ assert.Empty(t, branch.Commit.Verification.Signature)
+ }))
+ t.Run("CreateCRUDFile-Never", crudActionCreateFile(
+ t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
+ assert.False(t, response.Verification.Verified)
+ }))
+ t.Run("CreateCRUDFile-Never", crudActionCreateFile(
+ t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
+ assert.False(t, response.Verification.Verified)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
+ t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
+ assert.False(t, response.Verification.Verified)
+ }))
+ t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
+ t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
+ assert.False(t, response.Verification.Verified)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.CRUDActions = []string{"never"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ t.Run("CreateCRUDFile-Never", crudActionCreateFile(
+ t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
+ assert.False(t, response.Verification.Verified)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.CRUDActions = []string{"always"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ t.Run("CreateCRUDFile-Always", crudActionCreateFile(
+ t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
+ assert.NotNil(t, response.Verification)
+ if response.Verification == nil {
+ assert.FailNow(t, "no verification provided with response! %v", response)
+ return
+ }
+ assert.True(t, response.Verification.Verified)
+ if !response.Verification.Verified {
+ t.FailNow()
+ return
+ }
+ assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile(
+ t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
+ assert.NotNil(t, response.Verification)
+ if response.Verification == nil {
+ assert.FailNow(t, "no verification provided with response! %v", response)
+ return
+ }
+ assert.True(t, response.Verification.Verified)
+ if !response.Verification.Verified {
+ t.FailNow()
+ return
+ }
+ assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
+ t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
+ assert.NotNil(t, response.Verification)
+ if response.Verification == nil {
+ assert.FailNow(t, "no verification provided with response! %v", response)
+ return
+ }
+ assert.True(t, response.Verification.Verified)
+ if !response.Verification.Verified {
+ t.FailNow()
+ return
+ }
+ assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.InitialCommit = []string{"always"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("AlwaysSign-Initial", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-always")
+ t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
+ t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
+ assert.NotNil(t, branch.Commit)
+ if branch.Commit == nil {
+ assert.FailNow(t, "no commit provided with branch! %v", branch)
+ return
+ }
+ assert.NotNil(t, branch.Commit.Verification)
+ if branch.Commit.Verification == nil {
+ assert.FailNow(t, "no verification provided with branch commit! %v", branch.Commit)
+ return
+ }
+ assert.True(t, branch.Commit.Verification.Verified)
+ if !branch.Commit.Verification.Verified {
+ t.FailNow()
+ return
+ }
+ assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.CRUDActions = []string{"never"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-always-never")
+ t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
+ t.Run("CreateCRUDFile-Never", crudActionCreateFile(
+ t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
+ assert.False(t, response.Verification.Verified)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+ t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-always-parent")
+ t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
+ t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
+ t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
+ assert.True(t, response.Verification.Verified)
+ if !response.Verification.Verified {
+ t.FailNow()
+ return
+ }
+ assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.CRUDActions = []string{"always"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-always-always")
+ t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
+ t.Run("CreateCRUDFile-Always", crudActionCreateFile(
+ t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
+ assert.True(t, response.Verification.Verified)
+ if !response.Verification.Verified {
+ t.FailNow()
+ return
+ }
+ assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ })
+ }, false)
+ var pr api.PullRequest
+ setting.Repository.Signing.Merges = []string{"commitssigned"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("UnsignedMerging", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ var err error
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
+ assert.NoError(t, err)
+ })
+ t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
+ t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
+ assert.NotNil(t, branch.Commit)
+ assert.NotNil(t, branch.Commit.Verification)
+ assert.False(t, branch.Commit.Verification.Verified)
+ assert.Empty(t, branch.Commit.Verification.Signature)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.Merges = []string{"basesigned"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("BaseSignedMerging", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ var err error
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
+ assert.NoError(t, err)
+ })
+ t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
+ t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
+ assert.NotNil(t, branch.Commit)
+ assert.NotNil(t, branch.Commit.Verification)
+ assert.False(t, branch.Commit.Verification.Verified)
+ assert.Empty(t, branch.Commit.Verification.Signature)
+ }))
+ })
+ }, false)
+ setting.Repository.Signing.Merges = []string{"commitssigned"}
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ u.Path = baseAPITestContext.GitPath()
+
+ t.Run("CommitsSignedMerging", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ var err error
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
+ assert.NoError(t, err)
+ })
+ t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
+ t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
+ assert.NotNil(t, branch.Commit)
+ assert.NotNil(t, branch.Commit.Verification)
+ assert.True(t, branch.Commit.Verification.Verified)
+ }))
+ })
+ }, false)
+}
+
+func crudActionCreateFile(t *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
+ return doAPICreateFile(ctx, path, &api.CreateFileOptions{
+ FileOptions: api.FileOptions{
+ BranchName: from,
+ NewBranchName: to,
+ Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path),
+ Author: api.Identity{
+ Name: user.FullName,
+ Email: user.Email,
+ },
+ Committer: api.Identity{
+ Name: user.FullName,
+ Email: user.Email,
+ },
+ },
+ Content: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))),
+ }, callback...)
+}
+
+func importTestingKey(tmpDir, name, email string) (*openpgp.Entity, error) {
+ if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil {
+ return nil, err
+ }
+ keyringFile, err := os.Open("tests/integration/private-testing.key")
+ if err != nil {
+ return nil, err
+ }
+ defer keyringFile.Close()
+
+ block, err := armor.Decode(keyringFile)
+ if err != nil {
+ return nil, err
+ }
+
+ keyring, err := openpgp.ReadKeyRing(block.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Keyring access failed: '%v'", err)
+ }
+
+ // There should only be one entity in this file.
+ return keyring[0], nil
+}
diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go
new file mode 100644
index 0000000000..35d61f7b3e
--- /dev/null
+++ b/tests/integration/html_helper.go
@@ -0,0 +1,60 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/stretchr/testify/assert"
+)
+
+// HTMLDoc struct
+type HTMLDoc struct {
+ doc *goquery.Document
+}
+
+// NewHTMLParser parse html file
+func NewHTMLParser(t testing.TB, body *bytes.Buffer) *HTMLDoc {
+ t.Helper()
+ doc, err := goquery.NewDocumentFromReader(body)
+ assert.NoError(t, err)
+ return &HTMLDoc{doc: doc}
+}
+
+// GetInputValueByID for get input value by id
+func (doc *HTMLDoc) GetInputValueByID(id string) string {
+ text, _ := doc.doc.Find("#" + id).Attr("value")
+ return text
+}
+
+// GetInputValueByName for get input value by name
+func (doc *HTMLDoc) GetInputValueByName(name string) string {
+ text, _ := doc.doc.Find("input[name=\"" + name + "\"]").Attr("value")
+ return text
+}
+
+// Find gets the descendants of each element in the current set of
+// matched elements, filtered by a selector. It returns a new Selection
+// object containing these matched elements.
+func (doc *HTMLDoc) Find(selector string) *goquery.Selection {
+ return doc.doc.Find(selector)
+}
+
+// GetCSRF for getting CSRF token value from input
+func (doc *HTMLDoc) GetCSRF() string {
+ return doc.GetInputValueByName("_csrf")
+}
+
+// AssertElement check if element by selector exists or does not exist depending on checkExists
+func (doc *HTMLDoc) AssertElement(t testing.TB, selector string, checkExists bool) {
+ sel := doc.doc.Find(selector)
+ if checkExists {
+ assert.Equal(t, 1, sel.Length())
+ } else {
+ assert.Equal(t, 0, sel.Length())
+ }
+}
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
new file mode 100644
index 0000000000..8fc8a854a7
--- /dev/null
+++ b/tests/integration/integration_test.go
@@ -0,0 +1,407 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "hash"
+ "hash/fnv"
+ "io"
+ "net/http"
+ "net/http/cookiejar"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/stretchr/testify/assert"
+)
+
+var c *web.Route
+
+type NilResponseRecorder struct {
+ httptest.ResponseRecorder
+ Length int
+}
+
+func (n *NilResponseRecorder) Write(b []byte) (int, error) {
+ n.Length += len(b)
+ return len(b), nil
+}
+
+// NewRecorder returns an initialized ResponseRecorder.
+func NewNilResponseRecorder() *NilResponseRecorder {
+ return &NilResponseRecorder{
+ ResponseRecorder: *httptest.NewRecorder(),
+ }
+}
+
+type NilResponseHashSumRecorder struct {
+ httptest.ResponseRecorder
+ Hash hash.Hash
+ Length int
+}
+
+func (n *NilResponseHashSumRecorder) Write(b []byte) (int, error) {
+ _, _ = n.Hash.Write(b)
+ n.Length += len(b)
+ return len(b), nil
+}
+
+// NewRecorder returns an initialized ResponseRecorder.
+func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
+ return &NilResponseHashSumRecorder{
+ Hash: fnv.New32(),
+ ResponseRecorder: *httptest.NewRecorder(),
+ }
+}
+
+func TestMain(m *testing.M) {
+ defer log.Close()
+
+ managerCtx, cancel := context.WithCancel(context.Background())
+ graceful.InitManager(managerCtx)
+ defer cancel()
+
+ tests.InitTest(true)
+ c = routers.NormalRoutes(context.TODO())
+
+ // integration test settings...
+ if setting.Cfg != nil {
+ testingCfg := setting.Cfg.Section("integration-tests")
+ tests.SlowTest = testingCfg.Key("SLOW_TEST").MustDuration(tests.SlowTest)
+ tests.SlowFlush = testingCfg.Key("SLOW_FLUSH").MustDuration(tests.SlowFlush)
+ }
+
+ if os.Getenv("GITEA_SLOW_TEST_TIME") != "" {
+ duration, err := time.ParseDuration(os.Getenv("GITEA_SLOW_TEST_TIME"))
+ if err == nil {
+ tests.SlowTest = duration
+ }
+ }
+
+ if os.Getenv("GITEA_SLOW_FLUSH_TIME") != "" {
+ duration, err := time.ParseDuration(os.Getenv("GITEA_SLOW_FLUSH_TIME"))
+ if err == nil {
+ tests.SlowFlush = duration
+ }
+ }
+
+ os.Unsetenv("GIT_AUTHOR_NAME")
+ os.Unsetenv("GIT_AUTHOR_EMAIL")
+ os.Unsetenv("GIT_AUTHOR_DATE")
+ os.Unsetenv("GIT_COMMITTER_NAME")
+ os.Unsetenv("GIT_COMMITTER_EMAIL")
+ os.Unsetenv("GIT_COMMITTER_DATE")
+
+ err := unittest.InitFixtures(
+ unittest.FixturesOptions{
+ Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
+ },
+ )
+ if err != nil {
+ fmt.Printf("Error initializing test database: %v\n", err)
+ os.Exit(1)
+ }
+ exitCode := m.Run()
+
+ tests.WriterCloser.Reset()
+
+ if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
+ fmt.Printf("util.RemoveAll: %v\n", err)
+ os.Exit(1)
+ }
+ if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
+ fmt.Printf("Unable to remove repo indexer: %v\n", err)
+ os.Exit(1)
+ }
+
+ os.Exit(exitCode)
+}
+
+type TestSession struct {
+ jar http.CookieJar
+}
+
+func (s *TestSession) GetCookie(name string) *http.Cookie {
+ baseURL, err := url.Parse(setting.AppURL)
+ if err != nil {
+ return nil
+ }
+
+ for _, c := range s.jar.Cookies(baseURL) {
+ if c.Name == name {
+ return c
+ }
+ }
+ return nil
+}
+
+func (s *TestSession) MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.ResponseRecorder {
+ t.Helper()
+ baseURL, err := url.Parse(setting.AppURL)
+ assert.NoError(t, err)
+ for _, c := range s.jar.Cookies(baseURL) {
+ req.AddCookie(c)
+ }
+ resp := MakeRequest(t, req, expectedStatus)
+
+ ch := http.Header{}
+ ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
+ cr := http.Request{Header: ch}
+ s.jar.SetCookies(baseURL, cr.Cookies())
+
+ return resp
+}
+
+func (s *TestSession) MakeRequestNilResponseRecorder(t testing.TB, req *http.Request, expectedStatus int) *NilResponseRecorder {
+ t.Helper()
+ baseURL, err := url.Parse(setting.AppURL)
+ assert.NoError(t, err)
+ for _, c := range s.jar.Cookies(baseURL) {
+ req.AddCookie(c)
+ }
+ resp := MakeRequestNilResponseRecorder(t, req, expectedStatus)
+
+ ch := http.Header{}
+ ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
+ cr := http.Request{Header: ch}
+ s.jar.SetCookies(baseURL, cr.Cookies())
+
+ return resp
+}
+
+func (s *TestSession) MakeRequestNilResponseHashSumRecorder(t testing.TB, req *http.Request, expectedStatus int) *NilResponseHashSumRecorder {
+ t.Helper()
+ baseURL, err := url.Parse(setting.AppURL)
+ assert.NoError(t, err)
+ for _, c := range s.jar.Cookies(baseURL) {
+ req.AddCookie(c)
+ }
+ resp := MakeRequestNilResponseHashSumRecorder(t, req, expectedStatus)
+
+ ch := http.Header{}
+ ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
+ cr := http.Request{Header: ch}
+ s.jar.SetCookies(baseURL, cr.Cookies())
+
+ return resp
+}
+
+const userPassword = "password"
+
+var loginSessionCache = make(map[string]*TestSession, 10)
+
+func emptyTestSession(t testing.TB) *TestSession {
+ t.Helper()
+ jar, err := cookiejar.New(nil)
+ assert.NoError(t, err)
+
+ return &TestSession{jar: jar}
+}
+
+func getUserToken(t testing.TB, userName string) string {
+ return getTokenForLoggedInUser(t, loginUser(t, userName))
+}
+
+func loginUser(t testing.TB, userName string) *TestSession {
+ t.Helper()
+ if session, ok := loginSessionCache[userName]; ok {
+ return session
+ }
+ session := loginUserWithPassword(t, userName, userPassword)
+ loginSessionCache[userName] = session
+ return session
+}
+
+func loginUserWithPassword(t testing.TB, userName, password string) *TestSession {
+ t.Helper()
+ req := NewRequest(t, "GET", "/user/login")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
+ "_csrf": doc.GetCSRF(),
+ "user_name": userName,
+ "password": password,
+ })
+ resp = MakeRequest(t, req, http.StatusSeeOther)
+
+ ch := http.Header{}
+ ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
+ cr := http.Request{Header: ch}
+
+ session := emptyTestSession(t)
+
+ baseURL, err := url.Parse(setting.AppURL)
+ assert.NoError(t, err)
+ session.jar.SetCookies(baseURL, cr.Cookies())
+
+ return session
+}
+
+// token has to be unique this counter take care of
+var tokenCounter int64
+
+func getTokenForLoggedInUser(t testing.TB, session *TestSession) string {
+ t.Helper()
+ tokenCounter++
+ req := NewRequest(t, "GET", "/user/settings/applications")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+ req = NewRequestWithValues(t, "POST", "/user/settings/applications", map[string]string{
+ "_csrf": doc.GetCSRF(),
+ "name": fmt.Sprintf("api-testing-token-%d", tokenCounter),
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+ req = NewRequest(t, "GET", "/user/settings/applications")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ token := htmlDoc.doc.Find(".ui.info p").Text()
+ return token
+}
+
+func NewRequest(t testing.TB, method, urlStr string) *http.Request {
+ t.Helper()
+ return NewRequestWithBody(t, method, urlStr, nil)
+}
+
+func NewRequestf(t testing.TB, method, urlFormat string, args ...interface{}) *http.Request {
+ t.Helper()
+ return NewRequest(t, method, fmt.Sprintf(urlFormat, args...))
+}
+
+func NewRequestWithValues(t testing.TB, method, urlStr string, values map[string]string) *http.Request {
+ t.Helper()
+ urlValues := url.Values{}
+ for key, value := range values {
+ urlValues[key] = []string{value}
+ }
+ req := NewRequestWithBody(t, method, urlStr, bytes.NewBufferString(urlValues.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ return req
+}
+
+func NewRequestWithJSON(t testing.TB, method, urlStr string, v interface{}) *http.Request {
+ t.Helper()
+
+ jsonBytes, err := json.Marshal(v)
+ assert.NoError(t, err)
+ req := NewRequestWithBody(t, method, urlStr, bytes.NewBuffer(jsonBytes))
+ req.Header.Add("Content-Type", "application/json")
+ return req
+}
+
+func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *http.Request {
+ t.Helper()
+ if !strings.HasPrefix(urlStr, "http") && !strings.HasPrefix(urlStr, "/") {
+ urlStr = "/" + urlStr
+ }
+ request, err := http.NewRequest(method, urlStr, body)
+ assert.NoError(t, err)
+ request.RequestURI = urlStr
+ return request
+}
+
+func AddBasicAuthHeader(request *http.Request, username string) *http.Request {
+ request.SetBasicAuth(username, userPassword)
+ return request
+}
+
+const NoExpectedStatus = -1
+
+func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.ResponseRecorder {
+ t.Helper()
+ recorder := httptest.NewRecorder()
+ c.ServeHTTP(recorder, req)
+ if expectedStatus != NoExpectedStatus {
+ if !assert.EqualValues(t, expectedStatus, recorder.Code,
+ "Request: %s %s", req.Method, req.URL.String()) {
+ logUnexpectedResponse(t, recorder)
+ }
+ }
+ return recorder
+}
+
+func MakeRequestNilResponseRecorder(t testing.TB, req *http.Request, expectedStatus int) *NilResponseRecorder {
+ t.Helper()
+ recorder := NewNilResponseRecorder()
+ c.ServeHTTP(recorder, req)
+ if expectedStatus != NoExpectedStatus {
+ if !assert.EqualValues(t, expectedStatus, recorder.Code,
+ "Request: %s %s", req.Method, req.URL.String()) {
+ logUnexpectedResponse(t, &recorder.ResponseRecorder)
+ }
+ }
+ return recorder
+}
+
+func MakeRequestNilResponseHashSumRecorder(t testing.TB, req *http.Request, expectedStatus int) *NilResponseHashSumRecorder {
+ t.Helper()
+ recorder := NewNilResponseHashSumRecorder()
+ c.ServeHTTP(recorder, req)
+ if expectedStatus != NoExpectedStatus {
+ if !assert.EqualValues(t, expectedStatus, recorder.Code,
+ "Request: %s %s", req.Method, req.URL.String()) {
+ logUnexpectedResponse(t, &recorder.ResponseRecorder)
+ }
+ }
+ return recorder
+}
+
+// logUnexpectedResponse logs the contents of an unexpected response.
+func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
+ t.Helper()
+ respBytes := recorder.Body.Bytes()
+ if len(respBytes) == 0 {
+ return
+ } else if len(respBytes) < 500 {
+ // if body is short, just log the whole thing
+ t.Log("Response:", string(respBytes))
+ return
+ }
+
+ // log the "flash" error message, if one exists
+ // we must create a new buffer, so that we don't "use up" resp.Body
+ htmlDoc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(respBytes))
+ if err != nil {
+ return // probably a non-HTML response
+ }
+ errMsg := htmlDoc.Find(".ui.negative.message").Text()
+ if len(errMsg) > 0 {
+ t.Log("A flash error message was found:", errMsg)
+ }
+}
+
+func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v interface{}) {
+ t.Helper()
+
+ decoder := json.NewDecoder(resp.Body)
+ assert.NoError(t, decoder.Decode(v))
+}
+
+func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
+ t.Helper()
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+ return doc.GetCSRF()
+}
diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go
new file mode 100644
index 0000000000..1f0f894ca4
--- /dev/null
+++ b/tests/integration/issue_test.go
@@ -0,0 +1,559 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "path"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/indexer/issues"
+ "code.gitea.io/gitea/modules/references"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/stretchr/testify/assert"
+)
+
+func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection {
+ issueList := htmlDoc.doc.Find(".issue.list")
+ assert.EqualValues(t, 1, issueList.Length())
+ return issueList.Find("li").Find(".title")
+}
+
+func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue {
+ href, exists := issueSelection.Attr("href")
+ assert.True(t, exists)
+ indexStr := href[strings.LastIndexByte(href, '/')+1:]
+ index, err := strconv.Atoi(indexStr)
+ assert.NoError(t, err, "Invalid issue href: %s", href)
+ return unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repoID, Index: int64(index)})
+}
+
+func assertMatch(t testing.TB, issue *issues_model.Issue, keyword string) {
+ matches := strings.Contains(strings.ToLower(issue.Title), keyword) ||
+ strings.Contains(strings.ToLower(issue.Content), keyword)
+ for _, comment := range issue.Comments {
+ matches = matches || strings.Contains(
+ strings.ToLower(comment.Content),
+ keyword,
+ )
+ }
+ assert.True(t, matches)
+}
+
+func TestNoLoginViewIssues(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/issues")
+ MakeRequest(t, req, http.StatusOK)
+}
+
+func TestViewIssuesSortByType(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ session := loginUser(t, user.Name)
+ req := NewRequest(t, "GET", repo.Link()+"/issues?type=created_by")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ issuesSelection := getIssuesSelection(t, htmlDoc)
+ expectedNumIssues := unittest.GetCount(t,
+ &issues_model.Issue{RepoID: repo.ID, PosterID: user.ID},
+ unittest.Cond("is_closed=?", false),
+ unittest.Cond("is_pull=?", false),
+ )
+ if expectedNumIssues > setting.UI.IssuePagingNum {
+ expectedNumIssues = setting.UI.IssuePagingNum
+ }
+ assert.EqualValues(t, expectedNumIssues, issuesSelection.Length())
+
+ issuesSelection.Each(func(_ int, selection *goquery.Selection) {
+ issue := getIssue(t, repo.ID, selection)
+ assert.EqualValues(t, user.ID, issue.PosterID)
+ })
+}
+
+func TestViewIssuesKeyword(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
+ RepoID: repo.ID,
+ Index: 1,
+ })
+ issues.UpdateIssueIndexer(issue)
+ time.Sleep(time.Second * 1)
+ const keyword = "first"
+ req := NewRequestf(t, "GET", "%s/issues?q=%s", repo.Link(), keyword)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ issuesSelection := getIssuesSelection(t, htmlDoc)
+ assert.EqualValues(t, 1, issuesSelection.Length())
+ issuesSelection.Each(func(_ int, selection *goquery.Selection) {
+ issue := getIssue(t, repo.ID, selection)
+ assert.False(t, issue.IsClosed)
+ assert.False(t, issue.IsPull)
+ assertMatch(t, issue, keyword)
+ })
+}
+
+func TestNoLoginViewIssue(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/issues/1")
+ MakeRequest(t, req, http.StatusOK)
+}
+
+func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string {
+ req := NewRequest(t, "GET", path.Join(user, repo, "issues", "new"))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
+ assert.True(t, exists, "The template has changed")
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "title": title,
+ "content": content,
+ })
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ issueURL := test.RedirectURL(resp)
+ req = NewRequest(t, "GET", issueURL)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ val := htmlDoc.doc.Find("#issue-title").Text()
+ assert.Equal(t, title, val)
+ val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
+ assert.Equal(t, content, val)
+
+ return issueURL
+}
+
+func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content, status string) int64 {
+ req := NewRequest(t, "GET", issueURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find("#comment-form").Attr("action")
+ assert.True(t, exists, "The template has changed")
+
+ commentCount := htmlDoc.doc.Find(".comment-list .comment .render-content").Length()
+
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "content": content,
+ "status": status,
+ })
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequest(t, "GET", test.RedirectURL(resp))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc = NewHTMLParser(t, resp.Body)
+
+ val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text()
+ assert.Equal(t, content, val)
+
+ idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")
+ idStr := idAttr[strings.LastIndexByte(idAttr, '-')+1:]
+ assert.True(t, has)
+ id, err := strconv.Atoi(idStr)
+ assert.NoError(t, err)
+ return int64(id)
+}
+
+func TestNewIssue(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ testNewIssue(t, session, "user2", "repo1", "Title", "Description")
+}
+
+func TestIssueCommentClose(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
+ testIssueAddComment(t, session, issueURL, "Test comment 1", "")
+ testIssueAddComment(t, session, issueURL, "Test comment 2", "")
+ testIssueAddComment(t, session, issueURL, "Test comment 3", "close")
+
+ // Validate that issue content has not been updated
+ req := NewRequest(t, "GET", issueURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ val := htmlDoc.doc.Find(".comment-list .comment .render-content p").First().Text()
+ assert.Equal(t, "Description", val)
+}
+
+func TestIssueReaction(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
+
+ req := NewRequest(t, "GET", issueURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "content": "8ball",
+ })
+ session.MakeRequest(t, req, http.StatusInternalServerError)
+ req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/react"), map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "content": "eyes",
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequestWithValues(t, "POST", path.Join(issueURL, "/reactions/unreact"), map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "content": "eyes",
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestIssueCrossReference(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // Issue that will be referenced
+ _, issueBase := testIssueWithBean(t, "user2", 1, "Title", "Description")
+
+ // Ref from issue title
+ issueRefURL, issueRef := testIssueWithBean(t, "user2", 1, fmt.Sprintf("Title ref #%d", issueBase.Index), "Description")
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+ IssueID: issueBase.ID,
+ RefRepoID: 1,
+ RefIssueID: issueRef.ID,
+ RefCommentID: 0,
+ RefIsPull: false,
+ RefAction: references.XRefActionNone,
+ })
+
+ // Edit title, neuter ref
+ testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref")
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+ IssueID: issueBase.ID,
+ RefRepoID: 1,
+ RefIssueID: issueRef.ID,
+ RefCommentID: 0,
+ RefIsPull: false,
+ RefAction: references.XRefActionNeutered,
+ })
+
+ // Ref from issue content
+ issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index))
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+ IssueID: issueBase.ID,
+ RefRepoID: 1,
+ RefIssueID: issueRef.ID,
+ RefCommentID: 0,
+ RefIsPull: false,
+ RefAction: references.XRefActionNone,
+ })
+
+ // Edit content, neuter ref
+ testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref")
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+ IssueID: issueBase.ID,
+ RefRepoID: 1,
+ RefIssueID: issueRef.ID,
+ RefCommentID: 0,
+ RefIsPull: false,
+ RefAction: references.XRefActionNeutered,
+ })
+
+ // Ref from a comment
+ session := loginUser(t, "user2")
+ commentID := testIssueAddComment(t, session, issueRefURL, fmt.Sprintf("Adding ref from comment #%d", issueBase.Index), "")
+ comment := &issues_model.Comment{
+ IssueID: issueBase.ID,
+ RefRepoID: 1,
+ RefIssueID: issueRef.ID,
+ RefCommentID: commentID,
+ RefIsPull: false,
+ RefAction: references.XRefActionNone,
+ }
+ unittest.AssertExistsAndLoadBean(t, comment)
+
+ // Ref from a different repository
+ _, issueRef = testIssueWithBean(t, "user12", 10, "TitleXRef", fmt.Sprintf("Description ref user2/repo1#%d", issueBase.Index))
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
+ IssueID: issueBase.ID,
+ RefRepoID: 10,
+ RefIssueID: issueRef.ID,
+ RefCommentID: 0,
+ RefIsPull: false,
+ RefAction: references.XRefActionNone,
+ })
+}
+
+func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *issues_model.Issue) {
+ session := loginUser(t, user)
+ issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), title, content)
+ indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
+ index, err := strconv.Atoi(indexStr)
+ assert.NoError(t, err, "Invalid issue href: %s", issueURL)
+ issue := &issues_model.Issue{RepoID: repoID, Index: int64(index)}
+ unittest.AssertExistsAndLoadBean(t, issue)
+ return issueURL, issue
+}
+
+func testIssueChangeInfo(t *testing.T, user, issueURL, info, value string) {
+ session := loginUser(t, user)
+
+ req := NewRequest(t, "GET", issueURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ req = NewRequestWithValues(t, "POST", path.Join(issueURL, info), map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ info: value,
+ })
+ _ = session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestIssueRedirect(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+
+ // Test external tracker where style not set (shall default numeric)
+ req := NewRequest(t, "GET", path.Join("org26", "repo_external_tracker", "issues", "1"))
+ resp := session.MakeRequest(t, req, http.StatusSeeOther)
+ assert.Equal(t, "https://tracker.com/org26/repo_external_tracker/issues/1", test.RedirectURL(resp))
+
+ // Test external tracker with numeric style
+ req = NewRequest(t, "GET", path.Join("org26", "repo_external_tracker_numeric", "issues", "1"))
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ assert.Equal(t, "https://tracker.com/org26/repo_external_tracker_numeric/issues/1", test.RedirectURL(resp))
+
+ // Test external tracker with alphanumeric style (for a pull request)
+ req = NewRequest(t, "GET", path.Join("org26", "repo_external_tracker_alpha", "issues", "1"))
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ assert.Equal(t, "/"+path.Join("org26", "repo_external_tracker_alpha", "pulls", "1"), test.RedirectURL(resp))
+}
+
+func TestSearchIssues(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ expectedIssueCount := 15 // from the fixtures
+ if expectedIssueCount > setting.UI.IssuePagingNum {
+ expectedIssueCount = setting.UI.IssuePagingNum
+ }
+
+ link, _ := url.Parse("/issues/search")
+ req := NewRequest(t, "GET", link.String())
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiIssues []*api.Issue
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, expectedIssueCount)
+
+ since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801
+ before := time.Unix(999307200, 0).Format(time.RFC3339)
+ query := url.Values{}
+ query.Add("since", since)
+ query.Add("before", before)
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 8)
+ query.Del("since")
+ query.Del("before")
+
+ query.Add("state", "closed")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ query.Set("state", "all")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count"))
+ assert.Len(t, apiIssues, 17)
+
+ query.Add("limit", "5")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.EqualValues(t, "17", resp.Header().Get("X-Total-Count"))
+ assert.Len(t, apiIssues, 5)
+
+ query = url.Values{"assigned": {"true"}, "state": {"all"}}
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ query = url.Values{"milestones": {"milestone1"}, "state": {"all"}}
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 1)
+
+ query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}}
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ query = url.Values{"owner": {"user2"}} // user
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 6)
+
+ query = url.Values{"owner": {"user3"}} // organization
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 5)
+
+ query = url.Values{"owner": {"user3"}, "team": {"team1"}} // organization + team
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+}
+
+func TestSearchIssuesWithLabels(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ expectedIssueCount := 15 // from the fixtures
+ if expectedIssueCount > setting.UI.IssuePagingNum {
+ expectedIssueCount = setting.UI.IssuePagingNum
+ }
+
+ session := loginUser(t, "user1")
+ link, _ := url.Parse("/issues/search")
+ query := url.Values{}
+ var apiIssues []*api.Issue
+
+ link.RawQuery = query.Encode()
+ req := NewRequest(t, "GET", link.String())
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, expectedIssueCount)
+
+ query.Add("labels", "label1")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // multiple labels
+ query.Set("labels", "label1,label2")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // an org label
+ query.Set("labels", "orglabel4")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 1)
+
+ // org and repo label
+ query.Set("labels", "label2,orglabel4")
+ query.Add("state", "all")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+
+ // org and repo label which share the same issue
+ query.Set("labels", "label1,orglabel4")
+ link.RawQuery = query.Encode()
+ req = NewRequest(t, "GET", link.String())
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiIssues)
+ assert.Len(t, apiIssues, 2)
+}
+
+func TestGetIssueInfo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ assert.NoError(t, issue.LoadAttributes(db.DefaultContext))
+ assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix))
+ assert.Equal(t, api.StateOpen, issue.State())
+
+ session := loginUser(t, owner.Name)
+
+ urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index)
+ req := NewRequest(t, "GET", urlStr)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiIssue api.Issue
+ DecodeJSON(t, resp, &apiIssue)
+
+ assert.EqualValues(t, issue.ID, apiIssue.ID)
+}
+
+func TestUpdateIssueDeadline(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
+ repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
+ assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
+ assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
+ assert.Equal(t, api.StateOpen, issueBefore.State())
+
+ session := loginUser(t, owner.Name)
+
+ issueURL := fmt.Sprintf("%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
+ req := NewRequest(t, "GET", issueURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ urlStr := issueURL + "/deadline?_csrf=" + htmlDoc.GetCSRF()
+ req = NewRequestWithJSON(t, "POST", urlStr, map[string]string{
+ "due_date": "2022-04-06T00:00:00.000Z",
+ })
+
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ var apiIssue api.IssueDeadline
+ DecodeJSON(t, resp, &apiIssue)
+
+ assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02"))
+}
diff --git a/tests/integration/lfs_getobject_test.go b/tests/integration/lfs_getobject_test.go
new file mode 100644
index 0000000000..f2b0ac80c3
--- /dev/null
+++ b/tests/integration/lfs_getobject_test.go
@@ -0,0 +1,186 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "archive/zip"
+ "bytes"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers/web"
+ "code.gitea.io/gitea/tests"
+
+ gzipp "github.com/klauspost/compress/gzip"
+ "github.com/stretchr/testify/assert"
+)
+
+func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string {
+ pointer, err := lfs.GeneratePointer(bytes.NewReader(*content))
+ assert.NoError(t, err)
+
+ _, err = git_model.NewLFSMetaObject(&git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repositoryID})
+ assert.NoError(t, err)
+ contentStore := lfs.NewContentStore()
+ exist, err := contentStore.Exists(pointer)
+ assert.NoError(t, err)
+ if !exist {
+ err := contentStore.Put(pointer, bytes.NewReader(*content))
+ assert.NoError(t, err)
+ }
+ return pointer.Oid
+}
+
+func storeAndGetLfs(t *testing.T, content *[]byte, extraHeader *http.Header, expectedStatus int) *httptest.ResponseRecorder {
+ repo, err := repo_model.GetRepositoryByOwnerAndName("user2", "repo1")
+ assert.NoError(t, err)
+ oid := storeObjectInRepo(t, repo.ID, content)
+ defer git_model.RemoveLFSMetaObjectByOid(repo.ID, oid)
+
+ session := loginUser(t, "user2")
+
+ // Request OID
+ req := NewRequest(t, "GET", "/user2/repo1.git/info/lfs/objects/"+oid+"/test")
+ req.Header.Set("Accept-Encoding", "gzip")
+ if extraHeader != nil {
+ for key, values := range *extraHeader {
+ for _, value := range values {
+ req.Header.Add(key, value)
+ }
+ }
+ }
+
+ resp := session.MakeRequest(t, req, expectedStatus)
+
+ return resp
+}
+
+func checkResponseTestContentEncoding(t *testing.T, content *[]byte, resp *httptest.ResponseRecorder, expectGzip bool) {
+ contentEncoding := resp.Header().Get("Content-Encoding")
+ if !expectGzip || !setting.EnableGzip {
+ assert.NotContains(t, contentEncoding, "gzip")
+
+ result := resp.Body.Bytes()
+ assert.Equal(t, *content, result)
+ } else {
+ assert.Contains(t, contentEncoding, "gzip")
+ gzippReader, err := gzipp.NewReader(resp.Body)
+ assert.NoError(t, err)
+ result, err := io.ReadAll(gzippReader)
+ assert.NoError(t, err)
+ assert.Equal(t, *content, result)
+ }
+}
+
+func TestGetLFSSmall(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ content := []byte("A very small file\n")
+
+ resp := storeAndGetLfs(t, &content, nil, http.StatusOK)
+ checkResponseTestContentEncoding(t, &content, resp, false)
+}
+
+func TestGetLFSLarge(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ content := make([]byte, web.GzipMinSize*10)
+ for i := range content {
+ content[i] = byte(i % 256)
+ }
+
+ resp := storeAndGetLfs(t, &content, nil, http.StatusOK)
+ checkResponseTestContentEncoding(t, &content, resp, true)
+}
+
+func TestGetLFSGzip(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ b := make([]byte, web.GzipMinSize*10)
+ for i := range b {
+ b[i] = byte(i % 256)
+ }
+ outputBuffer := bytes.NewBuffer([]byte{})
+ gzippWriter := gzipp.NewWriter(outputBuffer)
+ gzippWriter.Write(b)
+ gzippWriter.Close()
+ content := outputBuffer.Bytes()
+
+ resp := storeAndGetLfs(t, &content, nil, http.StatusOK)
+ checkResponseTestContentEncoding(t, &content, resp, false)
+}
+
+func TestGetLFSZip(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ b := make([]byte, web.GzipMinSize*10)
+ for i := range b {
+ b[i] = byte(i % 256)
+ }
+ outputBuffer := bytes.NewBuffer([]byte{})
+ zipWriter := zip.NewWriter(outputBuffer)
+ fileWriter, err := zipWriter.Create("default")
+ assert.NoError(t, err)
+ fileWriter.Write(b)
+ zipWriter.Close()
+ content := outputBuffer.Bytes()
+
+ resp := storeAndGetLfs(t, &content, nil, http.StatusOK)
+ checkResponseTestContentEncoding(t, &content, resp, false)
+}
+
+func TestGetLFSRangeNo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ content := []byte("123456789\n")
+
+ resp := storeAndGetLfs(t, &content, nil, http.StatusOK)
+ assert.Equal(t, content, resp.Body.Bytes())
+}
+
+func TestGetLFSRange(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ content := []byte("123456789\n")
+
+ tests := []struct {
+ in string
+ out string
+ status int
+ }{
+ {"bytes=0-0", "1", http.StatusPartialContent},
+ {"bytes=0-1", "12", http.StatusPartialContent},
+ {"bytes=1-1", "2", http.StatusPartialContent},
+ {"bytes=1-3", "234", http.StatusPartialContent},
+ {"bytes=1-", "23456789\n", http.StatusPartialContent},
+ // end-range smaller than start-range is ignored
+ {"bytes=1-0", "23456789\n", http.StatusPartialContent},
+ {"bytes=0-10", "123456789\n", http.StatusPartialContent},
+ // end-range bigger than length-1 is ignored
+ {"bytes=0-11", "123456789\n", http.StatusPartialContent},
+ {"bytes=11-", "Requested Range Not Satisfiable", http.StatusRequestedRangeNotSatisfiable},
+ // incorrect header value cause whole header to be ignored
+ {"bytes=-", "123456789\n", http.StatusOK},
+ {"foobar", "123456789\n", http.StatusOK},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ h := http.Header{
+ "Range": []string{tt.in},
+ }
+ resp := storeAndGetLfs(t, &content, &h, tt.status)
+ if tt.status == http.StatusPartialContent || tt.status == http.StatusOK {
+ assert.Equal(t, tt.out, resp.Body.String())
+ } else {
+ var er lfs.ErrorResponse
+ err := json.Unmarshal(resp.Body.Bytes(), &er)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.out, er.Message)
+ }
+ })
+ }
+}
diff --git a/tests/integration/lfs_local_endpoint_test.go b/tests/integration/lfs_local_endpoint_test.go
new file mode 100644
index 0000000000..88c08c63db
--- /dev/null
+++ b/tests/integration/lfs_local_endpoint_test.go
@@ -0,0 +1,117 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func str2url(raw string) *url.URL {
+ u, _ := url.Parse(raw)
+ return u
+}
+
+func TestDetermineLocalEndpoint(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ root, _ := os.MkdirTemp("", "lfs_test")
+ defer os.RemoveAll(root)
+
+ rootdotgit, _ := os.MkdirTemp("", "lfs_test")
+ defer os.RemoveAll(rootdotgit)
+ os.Mkdir(filepath.Join(rootdotgit, ".git"), 0o700)
+
+ lfsroot, _ := os.MkdirTemp("", "lfs_test")
+ defer os.RemoveAll(lfsroot)
+
+ // Test cases
+ cases := []struct {
+ cloneurl string
+ lfsurl string
+ expected *url.URL
+ }{
+ // case 0
+ {
+ cloneurl: root,
+ lfsurl: "",
+ expected: str2url(fmt.Sprintf("file://%s", root)),
+ },
+ // case 1
+ {
+ cloneurl: root,
+ lfsurl: lfsroot,
+ expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
+ },
+ // case 2
+ {
+ cloneurl: "https://git.com/repo.git",
+ lfsurl: lfsroot,
+ expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
+ },
+ // case 3
+ {
+ cloneurl: rootdotgit,
+ lfsurl: "",
+ expected: str2url(fmt.Sprintf("file://%s", filepath.Join(rootdotgit, ".git"))),
+ },
+ // case 4
+ {
+ cloneurl: "",
+ lfsurl: rootdotgit,
+ expected: str2url(fmt.Sprintf("file://%s", filepath.Join(rootdotgit, ".git"))),
+ },
+ // case 5
+ {
+ cloneurl: rootdotgit,
+ lfsurl: rootdotgit,
+ expected: str2url(fmt.Sprintf("file://%s", filepath.Join(rootdotgit, ".git"))),
+ },
+ // case 6
+ {
+ cloneurl: fmt.Sprintf("file://%s", root),
+ lfsurl: "",
+ expected: str2url(fmt.Sprintf("file://%s", root)),
+ },
+ // case 7
+ {
+ cloneurl: fmt.Sprintf("file://%s", root),
+ lfsurl: fmt.Sprintf("file://%s", lfsroot),
+ expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
+ },
+ // case 8
+ {
+ cloneurl: root,
+ lfsurl: fmt.Sprintf("file://%s", lfsroot),
+ expected: str2url(fmt.Sprintf("file://%s", lfsroot)),
+ },
+ // case 9
+ {
+ cloneurl: "",
+ lfsurl: "/does/not/exist",
+ expected: nil,
+ },
+ // case 10
+ {
+ cloneurl: "",
+ lfsurl: "file:///does/not/exist",
+ expected: str2url("file:///does/not/exist"),
+ },
+ }
+
+ for n, c := range cases {
+ ep := lfs.DetermineEndpoint(c.cloneurl, c.lfsurl)
+
+ assert.Equal(t, c.expected, ep, "case %d: error should match", n)
+ }
+}
diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go
new file mode 100644
index 0000000000..4eb29f0cee
--- /dev/null
+++ b/tests/integration/links_test.go
@@ -0,0 +1,176 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "path"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLinksNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ links := []string{
+ "/explore/repos",
+ "/explore/repos?q=test",
+ "/explore/users",
+ "/explore/users?q=test",
+ "/explore/organizations",
+ "/explore/organizations?q=test",
+ "/",
+ "/user/sign_up",
+ "/user/login",
+ "/user/forgot_password",
+ "/api/swagger",
+ "/user2/repo1",
+ "/user2/repo1/",
+ "/user2/repo1/projects",
+ "/user2/repo1/projects/1",
+ "/assets/img/404.png",
+ "/assets/img/500.png",
+ }
+
+ for _, link := range links {
+ req := NewRequest(t, "GET", link)
+ MakeRequest(t, req, http.StatusOK)
+ }
+}
+
+func TestRedirectsNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ redirects := map[string]string{
+ "/user2/repo1/commits/master": "/user2/repo1/commits/branch/master",
+ "/user2/repo1/src/master": "/user2/repo1/src/branch/master",
+ "/user2/repo1/src/master/file.txt": "/user2/repo1/src/branch/master/file.txt",
+ "/user2/repo1/src/master/directory/file.txt": "/user2/repo1/src/branch/master/directory/file.txt",
+ "/user/avatar/Ghost/-1": "/assets/img/avatar_default.png",
+ "/api/v1/swagger": "/api/swagger",
+ }
+ for link, redirectLink := range redirects {
+ req := NewRequest(t, "GET", link)
+ resp := MakeRequest(t, req, http.StatusSeeOther)
+ assert.EqualValues(t, path.Join(setting.AppSubURL, redirectLink), test.RedirectURL(resp))
+ }
+}
+
+func TestNoLoginNotExist(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ links := []string{
+ "/user5/repo4/projects",
+ "/user5/repo4/projects/3",
+ }
+
+ for _, link := range links {
+ req := NewRequest(t, "GET", link)
+ MakeRequest(t, req, http.StatusNotFound)
+ }
+}
+
+func testLinksAsUser(userName string, t *testing.T) {
+ links := []string{
+ "/explore/repos",
+ "/explore/repos?q=test",
+ "/explore/users",
+ "/explore/users?q=test",
+ "/explore/organizations",
+ "/explore/organizations?q=test",
+ "/",
+ "/user/forgot_password",
+ "/api/swagger",
+ "/issues",
+ "/issues?type=your_repositories&repos=[0]&sort=&state=open",
+ "/issues?type=assigned&repos=[0]&sort=&state=open",
+ "/issues?type=your_repositories&repos=[0]&sort=&state=closed",
+ "/issues?type=assigned&repos=[]&sort=&state=closed",
+ "/issues?type=assigned&sort=&state=open",
+ "/issues?type=created_by&repos=[1,2]&sort=&state=closed",
+ "/issues?type=created_by&repos=[1,2]&sort=&state=open",
+ "/pulls",
+ "/pulls?type=your_repositories&repos=[2]&sort=&state=open",
+ "/pulls?type=assigned&repos=[]&sort=&state=open",
+ "/pulls?type=created_by&repos=[0]&sort=&state=open",
+ "/pulls?type=your_repositories&repos=[0]&sort=&state=closed",
+ "/pulls?type=assigned&repos=[0]&sort=&state=closed",
+ "/pulls?type=created_by&repos=[0]&sort=&state=closed",
+ "/milestones",
+ "/milestones?sort=mostcomplete&state=closed",
+ "/milestones?type=your_repositories&sort=mostcomplete&state=closed",
+ "/milestones?sort=&repos=[1]&state=closed",
+ "/milestones?sort=&repos=[1]&state=open",
+ "/milestones?repos=[0]&sort=mostissues&state=open",
+ "/notifications",
+ "/repo/create",
+ "/repo/migrate",
+ "/org/create",
+ "/user2",
+ "/user2?tab=stars",
+ "/user2?tab=activity",
+ "/user/settings",
+ "/user/settings/account",
+ "/user/settings/security",
+ "/user/settings/security/two_factor/enroll",
+ "/user/settings/keys",
+ "/user/settings/organization",
+ "/user/settings/repos",
+ }
+
+ session := loginUser(t, userName)
+ for _, link := range links {
+ req := NewRequest(t, "GET", link)
+ session.MakeRequest(t, req, http.StatusOK)
+ }
+
+ reqAPI := NewRequestf(t, "GET", "/api/v1/users/%s/repos", userName)
+ respAPI := MakeRequest(t, reqAPI, http.StatusOK)
+
+ var apiRepos []*api.Repository
+ DecodeJSON(t, respAPI, &apiRepos)
+
+ repoLinks := []string{
+ "",
+ "/issues",
+ "/pulls",
+ "/commits/branch/master",
+ "/graph",
+ "/settings",
+ "/settings/collaboration",
+ "/settings/branches",
+ "/settings/hooks",
+ // FIXME: below links should return 200 but 404 ??
+ //"/settings/hooks/git",
+ //"/settings/hooks/git/pre-receive",
+ //"/settings/hooks/git/update",
+ //"/settings/hooks/git/post-receive",
+ "/settings/keys",
+ "/releases",
+ "/releases/new",
+ //"/wiki/_pages",
+ "/wiki/?action=_new",
+ }
+
+ for _, repo := range apiRepos {
+ for _, link := range repoLinks {
+ req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s%s", userName, repo.Name, link))
+ session.MakeRequest(t, req, http.StatusOK)
+ }
+ }
+}
+
+func TestLinksLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ testLinksAsUser("user2", t)
+}
diff --git a/tests/integration/migrate_test.go b/tests/integration/migrate_test.go
new file mode 100644
index 0000000000..0fe4014344
--- /dev/null
+++ b/tests/integration/migrate_test.go
@@ -0,0 +1,98 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/migrations"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMigrateLocalPath(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+
+ old := setting.ImportLocalPaths
+ setting.ImportLocalPaths = true
+
+ lowercasePath, err := os.MkdirTemp("", "lowercase") // may not be lowercase because MkdirTemp creates a random directory name which may be mixedcase
+ assert.NoError(t, err)
+ defer os.RemoveAll(lowercasePath)
+
+ err = migrations.IsMigrateURLAllowed(lowercasePath, adminUser)
+ assert.NoError(t, err, "case lowercase path")
+
+ mixedcasePath, err := os.MkdirTemp("", "mIxeDCaSe")
+ assert.NoError(t, err)
+ defer os.RemoveAll(mixedcasePath)
+
+ err = migrations.IsMigrateURLAllowed(mixedcasePath, adminUser)
+ assert.NoError(t, err, "case mixedcase path")
+
+ setting.ImportLocalPaths = old
+}
+
+func TestMigrateGiteaForm(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
+ setting.Migrations.AllowLocalNetworks = true
+ AppVer := setting.AppVer
+ // Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string.
+ setting.AppVer = "1.16.0"
+ defer func() {
+ setting.Migrations.AllowLocalNetworks = AllowLocalNetworks
+ setting.AppVer = AppVer
+ migrations.Init()
+ }()
+ assert.NoError(t, migrations.Init())
+
+ ownerName := "user2"
+ repoName := "repo1"
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: ownerName})
+ session := loginUser(t, ownerName)
+ token := getTokenForLoggedInUser(t, session)
+
+ // Step 0: verify the repo is available
+ req := NewRequestf(t, "GET", fmt.Sprintf("/%s/%s", ownerName, repoName))
+ _ = session.MakeRequest(t, req, http.StatusOK)
+ // Step 1: get the Gitea migration form
+ req = NewRequestf(t, "GET", "/repo/migrate/?service_type=%d", structs.GiteaService)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ // Step 2: load the form
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find(`form.ui.form[action^="/repo/migrate"]`).Attr("action")
+ assert.True(t, exists, "The template has changed")
+ // Step 4: submit the migration to only migrate issues
+ migratedRepoName := "otherrepo"
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "service": fmt.Sprintf("%d", structs.GiteaService),
+ "clone_addr": fmt.Sprintf("%s%s/%s", u, ownerName, repoName),
+ "auth_token": token,
+ "issues": "on",
+ "repo_name": migratedRepoName,
+ "description": "",
+ "uid": fmt.Sprintf("%d", repoOwner.ID),
+ })
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ // Step 5: a redirection displays the migrated repository
+ loc := resp.Header().Get("Location")
+ assert.EqualValues(t, fmt.Sprintf("/%s/%s", ownerName, migratedRepoName), loc)
+ // Step 6: check the repo was created
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: migratedRepoName})
+ })
+}
diff --git a/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz
new file mode 100644
index 0000000000..1b676feda1
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/gitea-v1.6.4.mysql.sql.gz b/tests/integration/migration-test/gitea-v1.6.4.mysql.sql.gz
new file mode 100644
index 0000000000..30cca8b382
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.6.4.mysql.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/gitea-v1.6.4.postgres.sql.gz b/tests/integration/migration-test/gitea-v1.6.4.postgres.sql.gz
new file mode 100644
index 0000000000..bd66f6ba4f
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.6.4.postgres.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/gitea-v1.6.4.sqlite3.sql.gz b/tests/integration/migration-test/gitea-v1.6.4.sqlite3.sql.gz
new file mode 100644
index 0000000000..a777c53025
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.6.4.sqlite3.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz
new file mode 100644
index 0000000000..bd869cfa58
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/gitea-v1.7.0.mysql.sql.gz b/tests/integration/migration-test/gitea-v1.7.0.mysql.sql.gz
new file mode 100644
index 0000000000..d0ab10891c
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.7.0.mysql.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/gitea-v1.7.0.postgres.sql.gz b/tests/integration/migration-test/gitea-v1.7.0.postgres.sql.gz
new file mode 100644
index 0000000000..e4716c6b43
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.7.0.postgres.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/gitea-v1.7.0.sqlite3.sql.gz b/tests/integration/migration-test/gitea-v1.7.0.sqlite3.sql.gz
new file mode 100644
index 0000000000..3155249b07
--- /dev/null
+++ b/tests/integration/migration-test/gitea-v1.7.0.sqlite3.sql.gz
Binary files differ
diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go
new file mode 100644
index 0000000000..b631168340
--- /dev/null
+++ b/tests/integration/migration-test/migration_test.go
@@ -0,0 +1,338 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "compress/gzip"
+ "context"
+ "database/sql"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/migrations"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+ "xorm.io/xorm"
+)
+
+var currentEngine *xorm.Engine
+
+func initMigrationTest(t *testing.T) func() {
+ deferFn := tests.PrintCurrentTest(t, 2)
+ giteaRoot := base.SetupGiteaRoot()
+ if giteaRoot == "" {
+ tests.Printf("Environment variable $GITEA_ROOT not set\n")
+ os.Exit(1)
+ }
+ setting.AppPath = path.Join(giteaRoot, "gitea")
+ if _, err := os.Stat(setting.AppPath); err != nil {
+ tests.Printf("Could not find gitea binary at %s\n", setting.AppPath)
+ os.Exit(1)
+ }
+
+ giteaConf := os.Getenv("GITEA_CONF")
+ if giteaConf == "" {
+ tests.Printf("Environment variable $GITEA_CONF not set\n")
+ os.Exit(1)
+ } else if !path.IsAbs(giteaConf) {
+ setting.CustomConf = path.Join(giteaRoot, giteaConf)
+ } else {
+ setting.CustomConf = giteaConf
+ }
+
+ setting.LoadForTest()
+
+ assert.True(t, len(setting.RepoRootPath) != 0)
+ assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
+ assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
+ ownerDirs, err := os.ReadDir(setting.RepoRootPath)
+ if err != nil {
+ assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
+ }
+ for _, ownerDir := range ownerDirs {
+ if !ownerDir.Type().IsDir() {
+ continue
+ }
+ repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
+ if err != nil {
+ assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
+ }
+ for _, repoDir := range repoDirs {
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
+ }
+ }
+
+ assert.NoError(t, git.InitFull(context.Background()))
+ setting.InitDBConfig()
+ setting.NewLogServices(true)
+ return deferFn
+}
+
+func availableVersions() ([]string, error) {
+ migrationsDir, err := os.Open("tests/integration/migration-test")
+ if err != nil {
+ return nil, err
+ }
+ defer migrationsDir.Close()
+ versionRE, err := regexp.Compile("gitea-v(?P<version>.+)\\." + regexp.QuoteMeta(setting.Database.Type) + "\\.sql.gz")
+ if err != nil {
+ return nil, err
+ }
+
+ filenames, err := migrationsDir.Readdirnames(-1)
+ if err != nil {
+ return nil, err
+ }
+ versions := []string{}
+ for _, filename := range filenames {
+ if versionRE.MatchString(filename) {
+ substrings := versionRE.FindStringSubmatch(filename)
+ versions = append(versions, substrings[1])
+ }
+ }
+ sort.Strings(versions)
+ return versions, nil
+}
+
+func readSQLFromFile(version string) (string, error) {
+ filename := fmt.Sprintf("tests/integration/migration-test/gitea-v%s.%s.sql.gz", version, setting.Database.Type)
+
+ if _, err := os.Stat(filename); os.IsNotExist(err) {
+ return "", nil
+ }
+
+ file, err := os.Open(filename)
+ if err != nil {
+ return "", err
+ }
+ defer file.Close()
+
+ gr, err := gzip.NewReader(file)
+ if err != nil {
+ return "", err
+ }
+ defer gr.Close()
+
+ bytes, err := io.ReadAll(gr)
+ if err != nil {
+ return "", err
+ }
+ return string(charset.RemoveBOMIfPresent(bytes)), nil
+}
+
+func restoreOldDB(t *testing.T, version string) bool {
+ data, err := readSQLFromFile(version)
+ assert.NoError(t, err)
+ if len(data) == 0 {
+ tests.Printf("No db found to restore for %s version: %s\n", setting.Database.Type, version)
+ return false
+ }
+
+ switch {
+ case setting.Database.UseSQLite3:
+ util.Remove(setting.Database.Path)
+ err := os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm)
+ assert.NoError(t, err)
+
+ db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", setting.Database.Path, setting.Database.Timeout))
+ assert.NoError(t, err)
+ defer db.Close()
+
+ _, err = db.Exec(data)
+ assert.NoError(t, err)
+ db.Close()
+
+ case setting.Database.UseMySQL:
+ db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Host))
+ assert.NoError(t, err)
+ defer db.Close()
+
+ _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name))
+ assert.NoError(t, err)
+
+ _, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", setting.Database.Name))
+ assert.NoError(t, err)
+ db.Close()
+
+ db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?multiStatements=true",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name))
+ assert.NoError(t, err)
+ defer db.Close()
+
+ _, err = db.Exec(data)
+ assert.NoError(t, err)
+ db.Close()
+
+ case setting.Database.UsePostgreSQL:
+ var db *sql.DB
+ var err error
+ if setting.Database.Host[0] == '/' {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/?sslmode=%s&host=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.SSLMode, setting.Database.Host))
+ assert.NoError(t, err)
+ } else {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode))
+ assert.NoError(t, err)
+ }
+ defer db.Close()
+
+ _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name))
+ assert.NoError(t, err)
+
+ _, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", setting.Database.Name))
+ assert.NoError(t, err)
+ db.Close()
+
+ // Check if we need to setup a specific schema
+ if len(setting.Database.Schema) != 0 {
+ if setting.Database.Host[0] == '/' {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host))
+ } else {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
+ }
+ if !assert.NoError(t, err) {
+ return false
+ }
+ defer db.Close()
+
+ schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema))
+ if !assert.NoError(t, err) || !assert.NotEmpty(t, schrows) {
+ return false
+ }
+
+ if !schrows.Next() {
+ // Create and setup a DB schema
+ _, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s", setting.Database.Schema))
+ assert.NoError(t, err)
+ }
+ schrows.Close()
+
+ // Make the user's default search path the created schema; this will affect new connections
+ _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema))
+ assert.NoError(t, err)
+
+ db.Close()
+ }
+
+ if setting.Database.Host[0] == '/' {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host))
+ } else {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
+ }
+ assert.NoError(t, err)
+ defer db.Close()
+
+ _, err = db.Exec(data)
+ assert.NoError(t, err)
+ db.Close()
+
+ case setting.Database.UseMSSQL:
+ host, port := setting.ParseMSSQLHostPort(setting.Database.Host)
+ db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;",
+ host, port, "master", setting.Database.User, setting.Database.Passwd))
+ assert.NoError(t, err)
+ defer db.Close()
+
+ _, err = db.Exec("DROP DATABASE IF EXISTS [gitea]")
+ assert.NoError(t, err)
+
+ statements := strings.Split(data, "\nGO\n")
+ for _, statement := range statements {
+ if len(statement) > 5 && statement[:5] == "USE [" {
+ dbname := statement[5 : len(statement)-1]
+ db.Close()
+ db, err = sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;",
+ host, port, dbname, setting.Database.User, setting.Database.Passwd))
+ assert.NoError(t, err)
+ defer db.Close()
+ }
+ _, err = db.Exec(statement)
+ assert.NoError(t, err, "Failure whilst running: %s\nError: %v", statement, err)
+ }
+ db.Close()
+ }
+ return true
+}
+
+func wrappedMigrate(x *xorm.Engine) error {
+ currentEngine = x
+ return migrations.Migrate(x)
+}
+
+func doMigrationTest(t *testing.T, version string) {
+ defer tests.PrintCurrentTest(t)()
+ tests.Printf("Performing migration test for %s version: %s\n", setting.Database.Type, version)
+ if !restoreOldDB(t, version) {
+ return
+ }
+
+ setting.NewXORMLogService(false)
+
+ err := db.InitEngineWithMigration(context.Background(), wrappedMigrate)
+ assert.NoError(t, err)
+ currentEngine.Close()
+
+ beans, _ := db.NamesToBean()
+
+ err = db.InitEngineWithMigration(context.Background(), func(x *xorm.Engine) error {
+ currentEngine = x
+ return migrations.RecreateTables(beans...)(x)
+ })
+ assert.NoError(t, err)
+ currentEngine.Close()
+
+ // We do this a second time to ensure that there is not a problem with retained indices
+ err = db.InitEngineWithMigration(context.Background(), func(x *xorm.Engine) error {
+ currentEngine = x
+ return migrations.RecreateTables(beans...)(x)
+ })
+ assert.NoError(t, err)
+
+ currentEngine.Close()
+}
+
+func TestMigrations(t *testing.T) {
+ defer initMigrationTest(t)()
+
+ dialect := setting.Database.Type
+ versions, err := availableVersions()
+ assert.NoError(t, err)
+
+ if len(versions) == 0 {
+ tests.Printf("No old database versions available to migration test for %s\n", dialect)
+ return
+ }
+
+ tests.Printf("Preparing to test %d migrations for %s\n", len(versions), dialect)
+ for _, version := range versions {
+ t.Run(fmt.Sprintf("Migrate-%s-%s", dialect, version), func(t *testing.T) {
+ doMigrationTest(t, version)
+ })
+ }
+}
diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go
new file mode 100644
index 0000000000..707cf46fa0
--- /dev/null
+++ b/tests/integration/mirror_pull_test.go
@@ -0,0 +1,98 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/migration"
+ "code.gitea.io/gitea/modules/repository"
+ mirror_service "code.gitea.io/gitea/services/mirror"
+ release_service "code.gitea.io/gitea/services/release"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMirrorPull(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ repoPath := repo_model.RepoPath(user.Name, repo.Name)
+
+ opts := migration.MigrateOptions{
+ RepoName: "test_mirror",
+ Description: "Test mirror",
+ Private: false,
+ Mirror: true,
+ CloneAddr: repoPath,
+ Wiki: true,
+ Releases: false,
+ }
+
+ mirrorRepo, err := repository.CreateRepository(user, user, repository.CreateRepoOptions{
+ Name: opts.RepoName,
+ Description: opts.Description,
+ IsPrivate: opts.Private,
+ IsMirror: opts.Mirror,
+ Status: repo_model.RepositoryBeingMigrated,
+ })
+ assert.NoError(t, err)
+ assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation")
+
+ ctx := context.Background()
+
+ mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
+ assert.NoError(t, err)
+
+ gitRepo, err := git.OpenRepository(git.DefaultContext, repoPath)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ findOptions := repo_model.FindReleasesOptions{IncludeDrafts: true, IncludeTags: true}
+ initCount, err := repo_model.GetReleaseCountByRepoID(mirror.ID, findOptions)
+ assert.NoError(t, err)
+
+ assert.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{
+ RepoID: repo.ID,
+ Repo: repo,
+ PublisherID: user.ID,
+ Publisher: user,
+ TagName: "v0.2",
+ Target: "master",
+ Title: "v0.2 is released",
+ Note: "v0.2 is released",
+ IsDraft: false,
+ IsPrerelease: false,
+ IsTag: true,
+ }, nil, ""))
+
+ _, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
+ assert.NoError(t, err)
+
+ ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
+ assert.True(t, ok)
+
+ count, err := repo_model.GetReleaseCountByRepoID(mirror.ID, findOptions)
+ assert.NoError(t, err)
+ assert.EqualValues(t, initCount+1, count)
+
+ release, err := repo_model.GetRelease(repo.ID, "v0.2")
+ assert.NoError(t, err)
+ assert.NoError(t, release_service.DeleteReleaseByID(ctx, release.ID, user, true))
+
+ ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
+ assert.True(t, ok)
+
+ count, err = repo_model.GetReleaseCountByRepoID(mirror.ID, findOptions)
+ assert.NoError(t, err)
+ assert.EqualValues(t, initCount, count)
+}
diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go
new file mode 100644
index 0000000000..f2adf5f5a3
--- /dev/null
+++ b/tests/integration/mirror_push_test.go
@@ -0,0 +1,120 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/migrations"
+ mirror_service "code.gitea.io/gitea/services/mirror"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMirrorPush(t *testing.T) {
+ onGiteaRun(t, testMirrorPush)
+}
+
+func testMirrorPush(t *testing.T, u *url.URL) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.Migrations.AllowLocalNetworks = true
+ assert.NoError(t, migrations.Init())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ mirrorRepo, err := repository.CreateRepository(user, user, repository.CreateRepoOptions{
+ Name: "test-push-mirror",
+ })
+ assert.NoError(t, err)
+
+ ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)
+
+ doCreatePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword)(t)
+
+ mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
+ assert.NoError(t, err)
+ assert.Len(t, mirrors, 1)
+
+ ok := mirror_service.SyncPushMirror(context.Background(), mirrors[0].ID)
+ assert.True(t, ok)
+
+ srcGitRepo, err := git.OpenRepository(git.DefaultContext, srcRepo.RepoPath())
+ assert.NoError(t, err)
+ defer srcGitRepo.Close()
+
+ srcCommit, err := srcGitRepo.GetBranchCommit("master")
+ assert.NoError(t, err)
+
+ mirrorGitRepo, err := git.OpenRepository(git.DefaultContext, mirrorRepo.RepoPath())
+ assert.NoError(t, err)
+ defer mirrorGitRepo.Close()
+
+ mirrorCommit, err := mirrorGitRepo.GetBranchCommit("master")
+ assert.NoError(t, err)
+
+ assert.Equal(t, srcCommit.ID, mirrorCommit.ID)
+
+ // Cleanup
+ doRemovePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword, int(mirrors[0].ID))(t)
+ mirrors, _, err = repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
+ assert.NoError(t, err)
+ assert.Len(t, mirrors, 0)
+}
+
+func doCreatePushMirror(ctx APITestContext, address, username, password string) func(t *testing.T) {
+ return func(t *testing.T) {
+ csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
+
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
+ "_csrf": csrf,
+ "action": "push-mirror-add",
+ "push_mirror_address": address,
+ "push_mirror_username": username,
+ "push_mirror_password": password,
+ "push_mirror_interval": "0",
+ })
+ ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
+
+ flashCookie := ctx.Session.GetCookie("macaron_flash")
+ assert.NotNil(t, flashCookie)
+ assert.Contains(t, flashCookie.Value, "success")
+ }
+}
+
+func doRemovePushMirror(ctx APITestContext, address, username, password string, pushMirrorID int) func(t *testing.T) {
+ return func(t *testing.T) {
+ csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
+
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
+ "_csrf": csrf,
+ "action": "push-mirror-remove",
+ "push_mirror_id": strconv.Itoa(pushMirrorID),
+ "push_mirror_address": address,
+ "push_mirror_username": username,
+ "push_mirror_password": password,
+ "push_mirror_interval": "0",
+ })
+ ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
+
+ flashCookie := ctx.Session.GetCookie("macaron_flash")
+ assert.NotNil(t, flashCookie)
+ assert.Contains(t, flashCookie.Value, "success")
+ }
+}
diff --git a/tests/integration/nonascii_branches_test.go b/tests/integration/nonascii_branches_test.go
new file mode 100644
index 0000000000..ae69506f1b
--- /dev/null
+++ b/tests/integration/nonascii_branches_test.go
@@ -0,0 +1,214 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "path"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func testSrcRouteRedirect(t *testing.T, session *TestSession, user, repo, route, expectedLocation string, expectedStatus int) {
+ prefix := path.Join("/", user, repo, "src")
+
+ // Make request
+ req := NewRequest(t, "GET", path.Join(prefix, route))
+ resp := session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // Check Location header
+ location := resp.HeaderMap.Get("Location")
+ assert.Equal(t, path.Join(prefix, expectedLocation), location)
+
+ // Perform redirect
+ req = NewRequest(t, "GET", location)
+ session.MakeRequest(t, req, expectedStatus)
+}
+
+func setDefaultBranch(t *testing.T, session *TestSession, user, repo, branch string) {
+ location := path.Join("/", user, repo, "settings/branches")
+ csrf := GetCSRF(t, session, location)
+ req := NewRequestWithValues(t, "POST", location, map[string]string{
+ "_csrf": csrf,
+ "action": "default_branch",
+ "branch": branch,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
+func TestNonasciiBranches(t *testing.T) {
+ testRedirects := []struct {
+ from string
+ to string
+ status int
+ }{
+ // Branches
+ {
+ from: "master",
+ to: "branch/master",
+ status: http.StatusOK,
+ },
+ {
+ from: "master/README.md",
+ to: "branch/master/README.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "master/badfile",
+ to: "branch/master/badfile",
+ status: http.StatusNotFound, // it does not exists
+ },
+ {
+ from: "ГлавнаÑВетка",
+ to: "branch/%D0%93%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F%D0%92%D0%B5%D1%82%D0%BA%D0%B0",
+ status: http.StatusOK,
+ },
+ {
+ from: "а/б/в",
+ to: "branch/%D0%B0/%D0%B1/%D0%B2",
+ status: http.StatusOK,
+ },
+ {
+ from: "Grüßen/README.md",
+ to: "branch/Gr%C3%BC%C3%9Fen/README.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space",
+ to: "branch/Plus+Is+Not+Space",
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/Файл.md",
+ to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/and+it+is+valid.md",
+ to: "branch/Plus+Is+Not+Space/and+it+is+valid.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "ブランãƒ",
+ to: "branch/%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81",
+ status: http.StatusOK,
+ },
+ // Tags
+ {
+ from: "ТÑг",
+ to: "tag/%D0%A2%D1%8D%D0%B3",
+ status: http.StatusOK,
+ },
+ {
+ from: "Ð/人",
+ to: "tag/%D0%81/%E4%BA%BA",
+ status: http.StatusOK,
+ },
+ {
+ from: "ã‚¿ã‚°",
+ to: "tag/%E3%82%BF%E3%82%B0",
+ status: http.StatusOK,
+ },
+ {
+ from: "タグ/ファイル.md",
+ to: "tag/%E3%82%BF%E3%82%B0/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md",
+ status: http.StatusOK,
+ },
+ // Files
+ {
+ from: "README.md",
+ to: "branch/Plus+Is+Not+Space/README.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "Файл.md",
+ to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "ファイル.md",
+ to: "branch/Plus+Is+Not+Space/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md",
+ status: http.StatusNotFound, // it's not on default branch
+ },
+ // Same but url-encoded (few tests)
+ {
+ from: "%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81",
+ to: "branch/%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81",
+ status: http.StatusOK,
+ },
+ {
+ from: "%E3%82%BF%E3%82%b0",
+ to: "tag/%E3%82%BF%E3%82%B0",
+ status: http.StatusOK,
+ },
+ {
+ from: "%D0%A4%D0%B0%D0%B9%D0%BB.md",
+ to: "branch/Plus+Is+Not+Space/%D0%A4%D0%B0%D0%B9%D0%BB.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "%D0%81%2F%E4%BA%BA",
+ to: "tag/%D0%81/%E4%BA%BA",
+ status: http.StatusOK,
+ },
+ {
+ from: "Ð%2F%E4%BA%BA",
+ to: "tag/%D0%81/%E4%BA%BA",
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/%25%252525mightnotplaywell",
+ to: "branch/Plus+Is+Not+Space/%25%252525mightnotplaywell",
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/%25253Fisnotaquestion%25253F",
+ to: "branch/Plus+Is+Not+Space/%25253Fisnotaquestion%25253F",
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/" + url.PathEscape("%3Fis?and#afile"),
+ to: "branch/Plus+Is+Not+Space/" + url.PathEscape("%3Fis?and#afile"),
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/10%25.md",
+ to: "branch/Plus+Is+Not+Space/10%25.md",
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/" + url.PathEscape("This+file%20has 1space"),
+ to: "branch/Plus+Is+Not+Space/" + url.PathEscape("This+file%20has 1space"),
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/" + url.PathEscape("This+file%2520has 2 spaces"),
+ to: "branch/Plus+Is+Not+Space/" + url.PathEscape("This+file%2520has 2 spaces"),
+ status: http.StatusOK,
+ },
+ {
+ from: "Plus+Is+Not+Space/" + url.PathEscape("£15&$6.txt"),
+ to: "branch/Plus+Is+Not+Space/" + url.PathEscape("£15&$6.txt"),
+ status: http.StatusOK,
+ },
+ }
+
+ defer tests.PrepareTestEnv(t)()
+
+ user := "user2"
+ repo := "utf8"
+ session := loginUser(t, user)
+
+ setDefaultBranch(t, session, user, repo, "Plus+Is+Not+Space")
+
+ for _, test := range testRedirects {
+ testSrcRouteRedirect(t, session, user, repo, test.from, test.to, test.status)
+ }
+
+ setDefaultBranch(t, session, user, repo, "master")
+}
diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go
new file mode 100644
index 0000000000..7fa26c8147
--- /dev/null
+++ b/tests/integration/oauth_test.go
@@ -0,0 +1,261 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "io"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const defaultAuthorize = "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate"
+
+func TestNoClientID(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "GET", "/login/oauth/authorize")
+ ctx := loginUser(t, "user2")
+ ctx.MakeRequest(t, req, http.StatusBadRequest)
+}
+
+func TestLoginRedirect(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "GET", "/login/oauth/authorize")
+ assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login")
+}
+
+func TestShowAuthorize(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "GET", defaultAuthorize)
+ ctx := loginUser(t, "user4")
+ resp := ctx.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ htmlDoc.AssertElement(t, "#authorize-app", true)
+ htmlDoc.GetCSRF()
+}
+
+func TestRedirectWithExistingGrant(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequest(t, "GET", defaultAuthorize)
+ ctx := loginUser(t, "user1")
+ resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
+ u, err := resp.Result().Location()
+ assert.NoError(t, err)
+ assert.Equal(t, "thestate", u.Query().Get("state"))
+ assert.Truef(t, len(u.Query().Get("code")) > 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
+}
+
+func TestAccessTokenExchange(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ resp := MakeRequest(t, req, http.StatusOK)
+ type response struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int64 `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ }
+ parsed := new(response)
+
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
+ assert.True(t, len(parsed.AccessToken) > 10)
+ assert.True(t, len(parsed.RefreshToken) > 10)
+}
+
+func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ resp := MakeRequest(t, req, http.StatusOK)
+ type response struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int64 `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ }
+ parsed := new(response)
+
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
+ assert.True(t, len(parsed.AccessToken) > 10)
+ assert.True(t, len(parsed.RefreshToken) > 10)
+}
+
+func TestAccessTokenExchangeJSON(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "authcode",
+ })
+ MakeRequest(t, req, http.StatusBadRequest)
+}
+
+func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ // invalid client id
+ req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "???",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ MakeRequest(t, req, http.StatusBadRequest)
+ // invalid client secret
+ req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "???",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ MakeRequest(t, req, http.StatusBadRequest)
+ // invalid redirect uri
+ req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "???",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ MakeRequest(t, req, http.StatusBadRequest)
+ // invalid authorization code
+ req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "???",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ MakeRequest(t, req, http.StatusBadRequest)
+ // invalid grant_type
+ req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "???",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ MakeRequest(t, req, http.StatusBadRequest)
+}
+
+func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
+ resp := MakeRequest(t, req, http.StatusOK)
+ type response struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int64 `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ }
+ parsed := new(response)
+
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
+ assert.True(t, len(parsed.AccessToken) > 10)
+ assert.True(t, len(parsed.RefreshToken) > 10)
+
+ // use wrong client_secret
+ req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OmJsYWJsYQ==")
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ // missing header
+ req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ MakeRequest(t, req, http.StatusBadRequest)
+}
+
+func TestRefreshTokenInvalidation(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", // test PKCE additionally
+ })
+ resp := MakeRequest(t, req, http.StatusOK)
+ type response struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int64 `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ }
+ parsed := new(response)
+
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
+
+ // test without invalidation
+ setting.OAuth2.InvalidateRefreshTokens = false
+
+ refreshReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "refresh_token",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "refresh_token": parsed.RefreshToken,
+ })
+
+ bs, err := io.ReadAll(refreshReq.Body)
+ assert.NoError(t, err)
+
+ refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
+ MakeRequest(t, refreshReq, http.StatusOK)
+
+ refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
+ MakeRequest(t, refreshReq, http.StatusOK)
+
+ // test with invalidation
+ setting.OAuth2.InvalidateRefreshTokens = true
+ refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
+ MakeRequest(t, refreshReq, http.StatusOK)
+
+ refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
+ MakeRequest(t, refreshReq, http.StatusBadRequest)
+}
diff --git a/tests/integration/org_count_test.go b/tests/integration/org_count_test.go
new file mode 100644
index 0000000000..96f39924f1
--- /dev/null
+++ b/tests/integration/org_count_test.go
@@ -0,0 +1,147 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/url"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestOrgCounts(t *testing.T) {
+ onGiteaRun(t, testOrgCounts)
+}
+
+func testOrgCounts(t *testing.T, u *url.URL) {
+ orgOwner := "user2"
+ orgName := "testOrg"
+ orgCollaborator := "user4"
+ ctx := NewAPITestContext(t, orgOwner, "repo1")
+
+ var ownerCountRepos map[string]int
+ var collabCountRepos map[string]int
+
+ t.Run("GetTheOwnersNumRepos", doCheckOrgCounts(orgOwner, map[string]int{},
+ false,
+ func(_ *testing.T, calcOrgCounts map[string]int) {
+ ownerCountRepos = calcOrgCounts
+ },
+ ))
+ t.Run("GetTheCollaboratorsNumRepos", doCheckOrgCounts(orgCollaborator, map[string]int{},
+ false,
+ func(_ *testing.T, calcOrgCounts map[string]int) {
+ collabCountRepos = calcOrgCounts
+ },
+ ))
+
+ t.Run("CreatePublicTestOrganization", doAPICreateOrganization(ctx, &api.CreateOrgOption{
+ UserName: orgName,
+ Visibility: "public",
+ }))
+
+ // Following the creation of the organization, the orgName must appear in the counts with 0 repos
+ ownerCountRepos[orgName] = 0
+
+ t.Run("AssertNumRepos0ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
+
+ // the collaborator is not a collaborator yet
+ t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))
+
+ t.Run("CreateOrganizationPrivateRepo", doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{
+ Name: "privateTestRepo",
+ AutoInit: true,
+ Private: true,
+ }))
+
+ ownerCountRepos[orgName] = 1
+ t.Run("AssertNumRepos1ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
+
+ t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))
+
+ var testTeam api.Team
+
+ t.Run("CreateTeamForPublicTestOrganization", doAPICreateOrganizationTeam(ctx, orgName, &api.CreateTeamOption{
+ Name: "test",
+ Permission: "read",
+ Units: []string{"repo.code", "repo.issues", "repo.wiki", "repo.pulls", "repo.releases"},
+ CanCreateOrgRepo: true,
+ }, func(_ *testing.T, team api.Team) {
+ testTeam = team
+ }))
+
+ t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))
+
+ t.Run("AddCollboratorToTeam", doAPIAddUserToOrganizationTeam(ctx, testTeam.ID, orgCollaborator))
+
+ collabCountRepos[orgName] = 0
+ t.Run("AssertNumRepos0ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
+
+ // Now create a Public Repo
+ t.Run("CreateOrganizationPublicRepo", doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{
+ Name: "publicTestRepo",
+ AutoInit: true,
+ }))
+
+ ownerCountRepos[orgName] = 2
+ t.Run("AssertNumRepos2ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
+ collabCountRepos[orgName] = 1
+ t.Run("AssertNumRepos1ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
+
+ // Now add the testTeam to the privateRepo
+ t.Run("AddTestTeamToPrivateRepo", doAPIAddRepoToOrganizationTeam(ctx, testTeam.ID, orgName, "privateTestRepo"))
+
+ t.Run("AssertNumRepos2ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
+ collabCountRepos[orgName] = 2
+ t.Run("AssertNumRepos2ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
+}
+
+func doCheckOrgCounts(username string, orgCounts map[string]int, strict bool, callback ...func(*testing.T, map[string]int)) func(t *testing.T) {
+ canonicalCounts := make(map[string]int, len(orgCounts))
+
+ for key, value := range orgCounts {
+ newKey := strings.TrimSpace(strings.ToLower(key))
+ canonicalCounts[newKey] = value
+ }
+
+ return func(t *testing.T) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: username,
+ })
+
+ orgs, err := organization.FindOrgs(organization.FindOrgOptions{
+ UserID: user.ID,
+ IncludePrivate: true,
+ })
+ assert.NoError(t, err)
+
+ calcOrgCounts := map[string]int{}
+
+ for _, org := range orgs {
+ calcOrgCounts[org.LowerName] = org.NumRepos
+ count, ok := canonicalCounts[org.LowerName]
+ if ok {
+ assert.True(t, count == org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count)
+ } else {
+ assert.False(t, strict, "Did not expect to see %s with count %d", org.Name, org.NumRepos)
+ }
+ }
+
+ for key, value := range orgCounts {
+ _, seen := calcOrgCounts[strings.TrimSpace(strings.ToLower(key))]
+ assert.True(t, seen, "Expected to see %s with %d but did not", key, value)
+ }
+
+ if len(callback) > 0 {
+ callback[0](t, calcOrgCounts)
+ }
+ }
+}
diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go
new file mode 100644
index 0000000000..d04fcf7f57
--- /dev/null
+++ b/tests/integration/org_test.go
@@ -0,0 +1,224 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestOrgRepos(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ var (
+ users = []string{"user1", "user2"}
+ cases = map[string][]string{
+ "alphabetically": {"repo21", "repo3", "repo5"},
+ "reversealphabetically": {"repo5", "repo3", "repo21"},
+ }
+ )
+
+ for _, user := range users {
+ t.Run(user, func(t *testing.T) {
+ session := loginUser(t, user)
+ for sortBy, repos := range cases {
+ req := NewRequest(t, "GET", "/user3?sort="+sortBy)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ sel := htmlDoc.doc.Find("a.name")
+ assert.Len(t, repos, len(sel.Nodes))
+ for i := 0; i < len(repos); i++ {
+ assert.EqualValues(t, repos[i], strings.TrimSpace(sel.Eq(i).Text()))
+ }
+ }
+ })
+ }
+}
+
+func TestLimitedOrg(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // not logged in user
+ req := NewRequest(t, "GET", "/limited_org")
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org")
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequest(t, "GET", "/limited_org/private_repo_on_limited_org")
+ MakeRequest(t, req, http.StatusNotFound)
+
+ // login non-org member user
+ session := loginUser(t, "user2")
+ req = NewRequest(t, "GET", "/limited_org")
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org")
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", "/limited_org/private_repo_on_limited_org")
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // site admin
+ session = loginUser(t, "user1")
+ req = NewRequest(t, "GET", "/limited_org")
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org")
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", "/limited_org/private_repo_on_limited_org")
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestPrivateOrg(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // not logged in user
+ req := NewRequest(t, "GET", "/privated_org")
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org")
+ MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequest(t, "GET", "/privated_org/private_repo_on_private_org")
+ MakeRequest(t, req, http.StatusNotFound)
+
+ // login non-org member user
+ session := loginUser(t, "user2")
+ req = NewRequest(t, "GET", "/privated_org")
+ session.MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org")
+ session.MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequest(t, "GET", "/privated_org/private_repo_on_private_org")
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // non-org member who is collaborator on repo in private org
+ session = loginUser(t, "user4")
+ req = NewRequest(t, "GET", "/privated_org")
+ session.MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org") // colab of this repo
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", "/privated_org/private_repo_on_private_org")
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // site admin
+ session = loginUser(t, "user1")
+ req = NewRequest(t, "GET", "/privated_org")
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org")
+ session.MakeRequest(t, req, http.StatusOK)
+ req = NewRequest(t, "GET", "/privated_org/private_repo_on_private_org")
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestOrgMembers(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // not logged in user
+ req := NewRequest(t, "GET", "/org/org25/members")
+ MakeRequest(t, req, http.StatusOK)
+
+ // org member
+ session := loginUser(t, "user24")
+ req = NewRequest(t, "GET", "/org/org25/members")
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // site admin
+ session = loginUser(t, "user1")
+ req = NewRequest(t, "GET", "/org/org25/members")
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestOrgRestrictedUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // privated_org is a private org who has id 23
+ orgName := "privated_org"
+
+ // public_repo_on_private_org is a public repo on privated_org
+ repoName := "public_repo_on_private_org"
+
+ // user29 is a restricted user who is not a member of the organization
+ restrictedUser := "user29"
+
+ // #17003 reports a bug whereby adding a restricted user to a read-only team doesn't work
+
+ // assert restrictedUser cannot see the org or the public repo
+ restrictedSession := loginUser(t, restrictedUser)
+ req := NewRequest(t, "GET", fmt.Sprintf("/%s", orgName))
+ restrictedSession.MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName))
+ restrictedSession.MakeRequest(t, req, http.StatusNotFound)
+
+ // Therefore create a read-only team
+ adminSession := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, adminSession)
+
+ teamToCreate := &api.CreateTeamOption{
+ Name: "codereader",
+ Description: "Code Reader",
+ IncludesAllRepositories: true,
+ Permission: "read",
+ Units: []string{"repo.code"},
+ }
+
+ req = NewRequestWithJSON(t, "POST",
+ fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", orgName, token), teamToCreate)
+
+ var apiTeam api.Team
+
+ resp := adminSession.MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, &apiTeam)
+ checkTeamResponse(t, "CreateTeam_codereader", &apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
+ teamToCreate.Permission, teamToCreate.Units, nil)
+ checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
+ teamToCreate.Permission, teamToCreate.Units, nil)
+ // teamID := apiTeam.ID
+
+ // Now we need to add the restricted user to the team
+ req = NewRequest(t, "PUT",
+ fmt.Sprintf("/api/v1/teams/%d/members/%s?token=%s", apiTeam.ID, restrictedUser, token))
+ _ = adminSession.MakeRequest(t, req, http.StatusNoContent)
+
+ // Now we need to check if the restrictedUser can access the repo
+ req = NewRequest(t, "GET", fmt.Sprintf("/%s", orgName))
+ restrictedSession.MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName))
+ restrictedSession.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestTeamSearch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
+
+ var results TeamSearchResults
+
+ session := loginUser(t, user.Name)
+ csrf := GetCSRF(t, session, "/"+org.Name)
+ req := NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "_team")
+ req.Header.Add("X-Csrf-Token", csrf)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &results)
+ assert.NotEmpty(t, results.Data)
+ assert.Len(t, results.Data, 2)
+ assert.Equal(t, "review_team", results.Data[0].Name)
+ assert.Equal(t, "test_team", results.Data[1].Name)
+
+ // no access if not organization member
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+ session = loginUser(t, user5.Name)
+ csrf = GetCSRF(t, session, "/"+org.Name)
+ req = NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "team")
+ req.Header.Add("X-Csrf-Token", csrf)
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/private-testing.key b/tests/integration/private-testing.key
new file mode 100644
index 0000000000..b3874eab80
--- /dev/null
+++ b/tests/integration/private-testing.key
@@ -0,0 +1,81 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lQVYBGG44vABDAC7VVdrVcU2CzI4P1vm0HtsgRCj9TsCpxjESleIheG/jrLjpVaF
+YrlVKQ0+q6HXOMcbjJnsm+N6hgZNqwaKTNC6+LJZMXHlPG8wUGrHgHyUZ03urYB6
+vjlJ70RUBu1+dB5yJcTOk7kMLx8/is9FlAEEY/G98aviv2m3My6B5SJ2BErjREIw
+eRnWFm+JDcga9nRi8ra/DMac45iQ4IcQcj0NDlCn3aY88nGa6o1+07h7wYwI3t8S
++pfITuJgWf2cYK49v9QVsBMR8XHuS8UDGFuJ1Y4KK5zMHWKhah/6isyWPSgiC0wo
+V7LZDJp/tN8IoQf2fchRQN+x0PBeVXdt3KGXqvsfk7hnwGDKjGMp4nTxL8PFhpG8
+KJP0tTA063bbnrGjVYHaulTBTSKS8R3Zk2utA8JUgTU6tkNFoh8rNLgh2xtw/Ci3
+kvKzTdikWxBfspYgrWloMyCTZwOHssARyarXgtysEI1hNpvgpJo0WZOMurYuFDIB
+kEqgnqe1b1B7ItcAEQEAAQAL/iNebgZkZ7sX6w/mmn3eL+dhCNjD5LPQA6OP2635
+hRFLKmhDn63IYXB8MzV5ZzGA1UrUxX0AQ7cu1cLVPwNelGwwp0+iv7vFqMKI9Fgd
+YKgORw8AsAi8oIlehNqOgkmFN/haPCm6h04PGYnANfkPhA+lpQ81MTw64oVFwwqg
+TdzVW6RED3EidCfRDZblRLoefQPvimRQz7DwYa48zhNjVjaAVOcUuJ26MovKrBNd
+eu/Wr48/MQPez0hw6FnDs9fSAtB/cLmSlSL3yBkDB4RHTne6amvemX5SyQqOSKLJ
+F+YM33yIN3NQNQtJUkjNkBWuIe+s8pxFuKTHNyulCe/ES0ivtnqaCJ/J/PPzn/3t
+2S5f1K26jqJEnu4SfCxG3xTbSMu9DIcDP6BkU6WK9dQCPyfWZ3r3QkgZjHt02HP9
+Gbzh2tSxBO3b4ujysdSB2l78I0s3XLWae6FPNNKG+zmlCV8mUEa+OFVjS60GrX83
+NQVfoyjNdSQkLlg3+bo5DFma+QYAwr/HXi06iC8dh23HkPkYedIOml70SPAQqvVj
+xYtZRRSXo98P+QtA2kX0G3/9f606n2qqA9JXc3m4euvE94oSp708M5xAkSfdsc6B
+QIDNrR5ty+f+WdhZAsW4Gu/XbQ5ndkRReTtc3UtzIrC0zg8egCoE0yMfCJWPS2nF
+QTdlsl+cXDSQj7UMfCP9cKSsTzdEAF/P5ALI7Y+W4va/gy/0czJne+ZNMxPWE+Gs
+00KJCbSfgktnYhVt/XdWKuRZ8ylZBgD2QHts6MHkfno/OUK3wYDB7zLMIBdLltlg
+wvp7CXh8hIxzNqxaAjGus1XAg+/7QbSey/t88CR9XQsekd/L8NIYaFOxSpVAe03V
+RaW2/EXtmKIHKoWBTQJLJle3mp+iUiVjzdmTyUAqhFaCBYVMBlSvBuC99jXnu3U3
+UcUelLDvP2ufMdeXhVU1Anfg45wqvyfPIAhpgYMmyprGpfkd2Sf2W1ThaTec0kI1
+cT7AtkrqijCGDgo9ohl8ojmRhRCl968F/imENQATANdkhbYJ0k1+Ubm690xYNN7u
+d+wnQzS9P/UPpMrC4H2esz9g+Nls7X6/jeGB6K0bpOYAUR1VlRfuXREJcy9bK9Q8
+gzfBC4XWELA726fc9YeJqWH4fI9SFx0AjVVx6VFwSiDcoYbX26CLZN+jY6Gx8kx6
+PrOf4tPCU+8EP5f/tYn/dwN9oQPoyM7bYyN/zcrupLhHON7ryFr++Kpiw0feBGbg
+kEP+0HWJ2cX1MvcqTurx344RVlmnEBesDuFstBhnaXRlYSA8Z2l0ZWFAZmFrZS5s
+b2NhbD6JAc4EEwEKADgWIQT3rIVBIbYw8mUW1p+Z3Yqpy9FcAQUCYbji8AIbAwUL
+CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCZ3Yqpy9FcAY6AC/9GUc0vGAmZ1N7P
+ThOxy3SvoIWJzycEu6DKdp4FlucKW9Rm66vCwPDg7XcQxZQTIWNPIGB3kln0yRdx
+zRtGLKIDPo2qW8kPrLN3GXToKX2mBb76duaShW34W1rUVY613olmtwLT+QqgRX+H
+x0rNNJloOh3kawwaMoYZy4B2vq7AZ5ybIsT4ROKgKPzAlajI4+jI+qKA5GSyP7Jq
+Tu254BCeg0v51p0VWIbGdgPyVkZkLtrlxN7s8UGDoTUAJgB/K3SOGNtQFSxnJba5
+q0YBxDUScd65b1+YCUHY+3FdC4/5168y4Zic9bBxeVu3jBwSVDvrELsWzIDNgHmP
+eyl/Nv+CTZDDKOtzpS823k7gC129rcxMk0mkIzAt/wG7N4zf0vpt02LZ/Ei/azqK
+xq782Fmc3un+pgQWJrlU2ZT7yHi6aJAfxfDpQZwz8qXGgdaFsumNylEWy9o80pJG
+8RYgM+phZL4INYIiHoWUuz2v+qK9jmhxtTLOpKDXxtGrz6aFWJadBVgEYbji8AEM
+AMDFivCjl7vGACeST4iboZw817uAJFOTOk3uOnXuAx5NLq/DbL3Cyhjictwxhxot
+U1MdAZOSOHlWPBJTiib1145rDTJCH6gwQNVaqn/V0i/Dc2Isua4YF0efztzwD2aH
+NX4RCDp74bQ08YTsAlCWHk7blg3NCU/y4maaxdJ26PsNrIiY0l5SC3oNiEAp2aWP
+Yf+plmQwqk+Z3laB5fkVz8Vca8TZle11/NZVVwrpq8rubPUYHC2KmabFLihcMCGv
+eTt3LCB7tDzohDmX/0vuqTD09YTv5gmIzU/tx4+qH4tVfCK/DKTxsxafY4KZY4kM
+hqrhuGWq8EAu4RUG6AzbSDJZnO1UAfzC9j/8upr3qxOXx/xhWKzixGrRXo9eK5eR
+1pqEj+XGH+f9bQiF/pEIojcUp45S0ZBaSBPj2W7TZbbHzqXYNzmXa3IVdz+9l7MB
+cRfIe67wt4h66/fmATe47KvHNRfKpyhFD2utdOSd61tKXo/bu/5LBath0mxMBPHd
+4QARAQABAAv5Aacf824U/LiW+JU4poVJFofEr22gQhwwIt9rnmZm80ak+L+o9MaR
+CN4WLzJN2X5b1B8FTAXerexR8bPy1QsvaN/yRMT23wW3j0IVVf5tbIM/6m6o5+fP
+zp7S5/zh8OvbXE7v6Qp2C19sgQqB/ugOmff9hSBF18A6II2Wq8uLtgKua5xof1kI
+5/1qNpH1SltcndPPKjbq8D7zk6kjoZCw5PJk1ShVcKwIjzDmS729qezZ6nm6sh7v
+BX70JUdHErQzBtcb+Y39nRC/7aQ/X5s73Iy9OsnAzzTSTtw1RgxgAYXxQKhQN5xP
+rzUdZqCSFicjLAPvY4PxQmIL+DS7tb/rrWUJAfr/9LcrzoOC5LaYFTuykq231ORs
+4oRfHmJqYAiMYQ7iXMtFVspxQWq/8qrBPmmEkS2oAnmd8Ld5hbd7sFBsS5GCW9a3
+UyQQ9WQECyvpgFOR9m746/bFjKMgG+aBHyKvndniF3XWjHWrzrbk5vAViMb+9Al+
+7MxSqZ/oNrvdBgDT6hTMwyNBvQwJ/Lev0S3XPDJmxg+Y8QIrNbBrXjA70yVeLFgr
+emDnfdAwuhmZ5vKRe2YcIyMIOagRIDUEWs8EyCvM2e+bF+I0meQvWT536Cm2TouI
+jCUIip4HRTwe7NAR50OMACtji8sbcmfnIfFMfGUS3dPpNGURhCEHxWB6hlvbkbkV
+CToTlMS/agY0sV4O4kWqWiaKgZRefJSiVfj6RDKs43SbNxhJu+DslU7PPlfv6SFJ
+nX9LWE6daLrpuF0GAOjf+kjqpFFgF50h3B7lCsSfxIKW587z93rkmccGKvZj4Qeq
+ahjekO6kxapYJhtjY9BOQdU0rzEPhh8bF39GE/iCfXVdIh1suqp3uQv9birgkWJN
+CROrHvk5NmlBBb4BDid0hY8hM3lEi+6rK2lhs4krpoHin/h852AI+YBzeAVYSqor
+fqEzCiPlX7f1EI3I6kPnGrgeIWcznOO0yXkM/QuKCDWZlaLDxu7Rc5lBnsmiChrT
+3HwOiyOFfU1Rib/TVQYAng1PxHZfIfC77cblAiv3SXjFtSDIfyueER3Ii11DyEfB
+zco+qbpqYiDEI7yLZFuyExEpT2GbHTTEn28aEZzZBv/aFRnVFPTMiyquFE7QKuLc
+aEpEYZE3qSiAUDAckfDblM1SHZAVP6CaStkoUigtYBND2F316MTNGGLtcJ4y9s1r
+soqvCJ/cx0lR359kljqCHyv+iMqeBttwTGjFbiNJ5as4ATA988FlR6PnB0cr+Lg2
+8X3xiRcAaxlLFcUOifpa3m6JAbYEGAEKACAWIQT3rIVBIbYw8mUW1p+Z3Yqpy9Fc
+AQUCYbji8AIbDAAKCRCZ3Yqpy9FcAT/pDACilZ8zPUs+MwwI0BI6dMWxmhusHwTx
+kdwbxt2TuCQE3DEftCTCaxO5f8hQ6CL9pxYw5mn/6p8ELUpindFxgzpBjUQZyynb
++ZA7LOK5gKw25vGTRcMFiWZOBnMEAifyywmG6XCPtio8i3/In95ix/Adi17tzdpy
+EfFfWTeDocTNPhIPhg9REteZ71eBW3qEbY2iCeG3XSpKhkj6obY7BL8xLT9iaezh
+C6Upzb3gvjEInoaMR2yra9fVugW32lCFgXr6UZ5osBqVNjXGcwBqxg5IAkt4R5v1
+vdt5h69cagkbdS0qSRbS56GctmxVnbWyuAuKON55BDri5BhO3V4GmIXXUW12dQhl
+1/P9+xMjHm424QlGL7jgEzOMR5CJFdDQ+osabA2iZAUEQ7Ut8SgREfCduqKqzJ7z
+Uvb3feuoW45VNBqv7op8hH1S8okFaCTuznrAPqGXxee0I3oTX1lbBW+IySoisWMC
+ZMtt+nu5oJo/m1bvhWiYLhW6WX8TcmRKD3s=
+=V9rS
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/integration/privateactivity_test.go b/tests/integration/privateactivity_test.go
new file mode 100644
index 0000000000..3f352e49c6
--- /dev/null
+++ b/tests/integration/privateactivity_test.go
@@ -0,0 +1,417 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ privateActivityTestAdmin = "user1"
+ privateActivityTestUser = "user2"
+)
+
+// user3 is an organization so it is not usable here
+const privateActivityTestOtherUser = "user4"
+
+// activity helpers
+
+func testPrivateActivityDoSomethingForActionEntries(t *testing.T) {
+ repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
+
+ session := loginUser(t, privateActivityTestUser)
+ token := getTokenForLoggedInUser(t, session)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
+ Body: "test",
+ Title: "test",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+}
+
+// private activity helpers
+
+func testPrivateActivityHelperEnablePrivateActivity(t *testing.T) {
+ session := loginUser(t, privateActivityTestUser)
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": privateActivityTestUser,
+ "email": privateActivityTestUser + "@example.com",
+ "language": "en-US",
+ "keep_activity_private": "1",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc *HTMLDoc) bool {
+ return htmlDoc.doc.Find(".feeds").Find(".news").Length() > 0
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesFromSession(t *testing.T, session *TestSession) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesFromPublic(t *testing.T) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc)
+}
+
+// heatmap UI helpers
+
+func testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc *HTMLDoc) bool {
+ return htmlDoc.doc.Find("#user-heatmap").Length() > 0
+}
+
+func testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t *testing.T, session *TestSession) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t *testing.T, session *TestSession) bool {
+ req := NewRequest(t, "GET", "/")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleHeatmapFromPublic(t *testing.T) bool {
+ req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+// heatmap API helpers
+
+func testPrivateActivityHelperHasHeatmapContentFromPublic(t *testing.T) bool {
+ req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityTestUser)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var items []*activities_model.UserHeatmapData
+ DecodeJSON(t, resp, &items)
+
+ return len(items) != 0
+}
+
+func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session *TestSession) bool {
+ token := getTokenForLoggedInUser(t, session)
+
+ req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap?token=%s", privateActivityTestUser, token)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ var items []*activities_model.UserHeatmapData
+ DecodeJSON(t, resp, &items)
+
+ return len(items) != 0
+}
+
+// check activity visibility if the visibility is enabled
+
+func TestPrivateActivityNoVisibleForPublic(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromPublic(t)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForUserItself(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForOtherUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+// check activity visibility if the visibility is disabled
+
+func TestPrivateActivityYesInvisibleForPublic(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromPublic(t)
+
+ assert.False(t, visible, "user should have no visible activities")
+}
+
+func TestPrivateActivityYesVisibleForUserItself(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityYesInvisibleForOtherUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.False(t, visible, "user should have no visible activities")
+}
+
+func TestPrivateActivityYesVisibleForAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible activities")
+}
+
+// check heatmap visibility if the visibility is enabled
+
+func TestPrivateActivityNoHeatmapVisibleForPublic(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ visible := testPrivateActivityHelperHasVisibleHeatmapFromPublic(t)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForUserItselfAtProfile(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForUserItselfAtDashboard(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForOtherUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+// check heatmap visibility if the visibility is disabled
+
+func TestPrivateActivityYesHeatmapInvisibleForPublic(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ visible := testPrivateActivityHelperHasVisibleHeatmapFromPublic(t)
+
+ assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapVisibleForUserItselfAtProfile(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapVisibleForUserItselfAtDashboard(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ visible := testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvisibleForOtherUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapVisibleForAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+ assert.True(t, visible, "user should have visible heatmap")
+}
+
+// check heatmap api provides content if the visibility is enabled
+
+func TestPrivateActivityNoHeatmapHasContentForPublic(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromPublic(t)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForUserItself(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForOtherUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "user should have heatmap content")
+}
+
+// check heatmap api provides no content if the visibility is disabled
+// this should be equal to the hidden heatmap at the UI
+
+func TestPrivateActivityYesHeatmapHasNoContentForPublic(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromPublic(t)
+
+ assert.False(t, hasContent, "user should have no heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForUserItself(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "user should see their own heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForOtherUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestOtherUser)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.False(t, hasContent, "other user should not see heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForAdmin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testPrivateActivityDoSomethingForActionEntries(t)
+ testPrivateActivityHelperEnablePrivateActivity(t)
+
+ session := loginUser(t, privateActivityTestAdmin)
+ hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+ assert.True(t, hasContent, "heatmap should show content for admin")
+}
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
new file mode 100644
index 0000000000..7934b6e77c
--- /dev/null
+++ b/tests/integration/pull_compare_test.go
@@ -0,0 +1,28 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPullCompare(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/user2/repo1/pulls")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find(".ui.three.column.grid").Find(".ui.green.button").Attr("href")
+ assert.True(t, exists, "The template has changed")
+
+ req = NewRequest(t, "GET", link)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.EqualValues(t, http.StatusOK, resp.Code)
+}
diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go
new file mode 100644
index 0000000000..24c73ab4e9
--- /dev/null
+++ b/tests/integration/pull_create_test.go
@@ -0,0 +1,163 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "path"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func testPullCreate(t *testing.T, session *TestSession, user, repo, branch, title string) *httptest.ResponseRecorder {
+ req := NewRequest(t, "GET", path.Join(user, repo))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ // Click the PR button to create a pull
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find("#new-pull-request").Parent().Attr("href")
+ assert.True(t, exists, "The template has changed")
+ if branch != "master" {
+ link = strings.Replace(link, ":master", ":"+branch, 1)
+ }
+
+ req = NewRequest(t, "GET", link)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ // Submit the form for creating the pull
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ link, exists = htmlDoc.doc.Find("form.ui.form").Attr("action")
+ assert.True(t, exists, "The template has changed")
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "title": title,
+ })
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ return resp
+}
+
+func TestPullCreate(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+
+ // check the redirected URL
+ url := resp.Header().Get("Location")
+ assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url)
+
+ // check .diff can be accessed and matches performed change
+ req := NewRequest(t, "GET", url+".diff")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.Regexp(t, `\+Hello, World \(Edited\)`, resp.Body)
+ assert.Regexp(t, "^diff", resp.Body)
+ assert.NotRegexp(t, "diff.*diff", resp.Body) // not two diffs, just one
+
+ // check .patch can be accessed and matches performed change
+ req = NewRequest(t, "GET", url+".patch")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ assert.Regexp(t, `\+Hello, World \(Edited\)`, resp.Body)
+ assert.Regexp(t, "diff", resp.Body)
+ assert.Regexp(t, `Subject: \[PATCH\] Update 'README.md'`, resp.Body)
+ assert.NotRegexp(t, "diff.*diff", resp.Body) // not two diffs, just one
+ })
+}
+
+func TestPullCreate_TitleEscape(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "<i>XSS PR</i>")
+
+ // check the redirected URL
+ url := resp.Header().Get("Location")
+ assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url)
+
+ // Edit title
+ req := NewRequest(t, "GET", url)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url")
+ assert.True(t, exists, "The template has changed")
+
+ req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "title": "<u>XSS PR</u>",
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequest(t, "GET", url)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ titleHTML, err := htmlDoc.doc.Find(".comment-list .timeline-item.event .text b").First().Html()
+ assert.NoError(t, err)
+ assert.Equal(t, "<strike>&lt;i&gt;XSS PR&lt;/i&gt;</strike>", titleHTML)
+ titleHTML, err = htmlDoc.doc.Find(".comment-list .timeline-item.event .text b").Next().Html()
+ assert.NoError(t, err)
+ assert.Equal(t, "&lt;u&gt;XSS PR&lt;/u&gt;", titleHTML)
+ })
+}
+
+func testUIDeleteBranch(t *testing.T, session *TestSession, ownerName, repoName, branchName string) {
+ relURL := "/" + path.Join(ownerName, repoName, "branches")
+ req := NewRequest(t, "GET", relURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ req = NewRequestWithValues(t, "POST", relURL+"/delete", map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "name": branchName,
+ })
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func testDeleteRepository(t *testing.T, session *TestSession, ownerName, repoName string) {
+ relURL := "/" + path.Join(ownerName, repoName, "settings")
+ req := NewRequest(t, "GET", relURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ req = NewRequestWithValues(t, "POST", relURL+"?action=delete", map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "repo_name": repoName,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
+func TestPullBranchDelete(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
+ testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
+ resp := testPullCreate(t, session, "user1", "repo1", "master1", "This is a pull title")
+
+ // check the redirected URL
+ url := resp.Header().Get("Location")
+ assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url)
+ req := NewRequest(t, "GET", url)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // delete head branch and confirm pull page is ok
+ testUIDeleteBranch(t, session, "user1", "repo1", "master1")
+ req = NewRequest(t, "GET", url)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // delete head repository and confirm pull page is ok
+ testDeleteRepository(t, session, "user1", "repo1")
+ req = NewRequest(t, "GET", url)
+ session.MakeRequest(t, req, http.StatusOK)
+ })
+}
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
new file mode 100644
index 0000000000..335dae4b38
--- /dev/null
+++ b/tests/integration/pull_merge_test.go
@@ -0,0 +1,423 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/git"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/services/pull"
+ repo_service "code.gitea.io/gitea/services/repository"
+ files_service "code.gitea.io/gitea/services/repository/files"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle) *httptest.ResponseRecorder {
+ req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link := path.Join(user, repo, "pulls", pullnum, "merge")
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "do": string(mergeStyle),
+ })
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ return resp
+}
+
+func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
+ req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ // Click the little green button to create a pull
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
+ assert.True(t, exists, "The template has changed, can not find delete button url")
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ return resp
+}
+
+func TestPullMerge(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
+ assert.NoError(t, err)
+ hookTasksLenBefore := len(hookTasks)
+
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+
+ elem := strings.Split(test.RedirectURL(resp), "/")
+ assert.EqualValues(t, "pulls", elem[3])
+ testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
+
+ hookTasks, err = webhook.HookTasks(1, 1)
+ assert.NoError(t, err)
+ assert.Len(t, hookTasks, hookTasksLenBefore+1)
+ })
+}
+
+func TestPullRebase(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
+ assert.NoError(t, err)
+ hookTasksLenBefore := len(hookTasks)
+
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+
+ elem := strings.Split(test.RedirectURL(resp), "/")
+ assert.EqualValues(t, "pulls", elem[3])
+ testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase)
+
+ hookTasks, err = webhook.HookTasks(1, 1)
+ assert.NoError(t, err)
+ assert.Len(t, hookTasks, hookTasksLenBefore+1)
+ })
+}
+
+func TestPullRebaseMerge(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
+ assert.NoError(t, err)
+ hookTasksLenBefore := len(hookTasks)
+
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+
+ elem := strings.Split(test.RedirectURL(resp), "/")
+ assert.EqualValues(t, "pulls", elem[3])
+ testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge)
+
+ hookTasks, err = webhook.HookTasks(1, 1)
+ assert.NoError(t, err)
+ assert.Len(t, hookTasks, hookTasksLenBefore+1)
+ })
+}
+
+func TestPullSquash(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
+ assert.NoError(t, err)
+ hookTasksLenBefore := len(hookTasks)
+
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
+
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+
+ elem := strings.Split(test.RedirectURL(resp), "/")
+ assert.EqualValues(t, "pulls", elem[3])
+ testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash)
+
+ hookTasks, err = webhook.HookTasks(1, 1)
+ assert.NoError(t, err)
+ assert.Len(t, hookTasks, hookTasksLenBefore+1)
+ })
+}
+
+func TestPullCleanUpAfterMerge(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
+
+ resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
+
+ elem := strings.Split(test.RedirectURL(resp), "/")
+ assert.EqualValues(t, "pulls", elem[3])
+ testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
+
+ // Check PR branch deletion
+ resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
+ respJSON := struct {
+ Redirect string
+ }{}
+ DecodeJSON(t, resp, &respJSON)
+
+ assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
+
+ elem = strings.Split(respJSON.Redirect, "/")
+ assert.EqualValues(t, "pulls", elem[3])
+
+ // Check branch deletion result
+ req := NewRequest(t, "GET", respJSON.Redirect)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
+
+ assert.EqualValues(t, "Branch 'user1/feature/test' has been deleted.", resultMsg)
+ })
+}
+
+func TestCantMergeWorkInProgress(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
+
+ req := NewRequest(t, "GET", resp.Header().Get("Location"))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
+ assert.NotEmpty(t, text, "Can't find WIP text")
+
+ assert.Contains(t, text, translation.NewLocale("en-US").Tr("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
+ assert.Contains(t, text, "[wip]", "Unable to find WIP text")
+ })
+}
+
+func TestCantMergeConflict(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
+
+ // Use API to create a conflicting pr
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
+ Head: "conflict",
+ Base: "base",
+ Title: "create a conflicting pr",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ // Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: "user1",
+ })
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
+ OwnerID: user1.ID,
+ Name: "repo1",
+ })
+
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+ HeadRepoID: repo1.ID,
+ BaseRepoID: repo1.ID,
+ HeadBranch: "conflict",
+ BaseBranch: "base",
+ })
+
+ gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
+ assert.NoError(t, err)
+
+ err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT")
+ assert.Error(t, err, "Merge should return an error due to conflict")
+ assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
+
+ err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT")
+ assert.Error(t, err, "Merge should return an error due to conflict")
+ assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
+ gitRepo.Close()
+ })
+}
+
+func TestCantMergeUnrelated(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
+
+ // Now we want to create a commit on a branch that is totally unrelated to our current head
+ // Drop down to pure code at this point
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: "user1",
+ })
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
+ OwnerID: user1.ID,
+ Name: "repo1",
+ })
+ path := repo_model.RepoPath(user1.Name, repo1.Name)
+
+ err := git.NewCommand(git.DefaultContext, "read-tree", "--empty").Run(&git.RunOpts{Dir: path})
+ assert.NoError(t, err)
+
+ stdin := bytes.NewBufferString("Unrelated File")
+ var stdout strings.Builder
+ err = git.NewCommand(git.DefaultContext, "hash-object", "-w", "--stdin").Run(&git.RunOpts{
+ Dir: path,
+ Stdin: stdin,
+ Stdout: &stdout,
+ })
+
+ assert.NoError(t, err)
+ sha := strings.TrimSpace(stdout.String())
+
+ _, _, err = git.NewCommand(git.DefaultContext, "update-index", "--add", "--replace", "--cacheinfo", "100644", sha, "somewher-over-the-rainbow").RunStdString(&git.RunOpts{Dir: path})
+ assert.NoError(t, err)
+
+ treeSha, _, err := git.NewCommand(git.DefaultContext, "write-tree").RunStdString(&git.RunOpts{Dir: path})
+ assert.NoError(t, err)
+ treeSha = strings.TrimSpace(treeSha)
+
+ commitTimeStr := time.Now().Format(time.RFC3339)
+ doerSig := user1.NewGitSig()
+ env := append(os.Environ(),
+ "GIT_AUTHOR_NAME="+doerSig.Name,
+ "GIT_AUTHOR_EMAIL="+doerSig.Email,
+ "GIT_AUTHOR_DATE="+commitTimeStr,
+ "GIT_COMMITTER_NAME="+doerSig.Name,
+ "GIT_COMMITTER_EMAIL="+doerSig.Email,
+ "GIT_COMMITTER_DATE="+commitTimeStr,
+ )
+
+ messageBytes := new(bytes.Buffer)
+ _, _ = messageBytes.WriteString("Unrelated")
+ _, _ = messageBytes.WriteString("\n")
+
+ stdout.Reset()
+ err = git.NewCommand(git.DefaultContext, "commit-tree", treeSha).
+ Run(&git.RunOpts{
+ Env: env,
+ Dir: path,
+ Stdin: messageBytes,
+ Stdout: &stdout,
+ })
+ assert.NoError(t, err)
+ commitSha := strings.TrimSpace(stdout.String())
+
+ _, _, err = git.NewCommand(git.DefaultContext, "branch", "unrelated", commitSha).RunStdString(&git.RunOpts{Dir: path})
+ assert.NoError(t, err)
+
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
+
+ // Use API to create a conflicting pr
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
+ Head: "unrelated",
+ Base: "base",
+ Title: "create an unrelated pr",
+ })
+ session.MakeRequest(t, req, http.StatusCreated)
+
+ // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
+ gitRepo, err := git.OpenRepository(git.DefaultContext, path)
+ assert.NoError(t, err)
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+ HeadRepoID: repo1.ID,
+ BaseRepoID: repo1.ID,
+ HeadBranch: "unrelated",
+ BaseBranch: "base",
+ })
+
+ err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED")
+ assert.Error(t, err, "Merge should return an error due to unrelated")
+ assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
+ gitRepo.Close()
+ })
+}
+
+func TestConflictChecking(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ // Create new clean repo to test conflict checking.
+ baseRepo, err := repo_service.CreateRepository(user, user, repo_module.CreateRepoOptions{
+ Name: "conflict-checking",
+ Description: "Tempo repo",
+ AutoInit: true,
+ Readme: "Default",
+ DefaultBranch: "main",
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, baseRepo)
+
+ // create a commit on new branch.
+ _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
+ TreePath: "important_file",
+ Message: "Add a important file",
+ Content: "Just a non-important file",
+ IsNewFile: true,
+ OldBranch: "main",
+ NewBranch: "important-secrets",
+ })
+ assert.NoError(t, err)
+
+ // create a commit on main branch.
+ _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
+ TreePath: "important_file",
+ Message: "Add a important file",
+ Content: "Not the same content :P",
+ IsNewFile: true,
+ OldBranch: "main",
+ NewBranch: "main",
+ })
+ assert.NoError(t, err)
+
+ // create Pull to merge the important-secrets branch into main branch.
+ pullIssue := &issues_model.Issue{
+ RepoID: baseRepo.ID,
+ Title: "PR with conflict!",
+ PosterID: user.ID,
+ Poster: user,
+ IsPull: true,
+ }
+
+ pullRequest := &issues_model.PullRequest{
+ HeadRepoID: baseRepo.ID,
+ BaseRepoID: baseRepo.ID,
+ HeadBranch: "important-secrets",
+ BaseBranch: "main",
+ HeadRepo: baseRepo,
+ BaseRepo: baseRepo,
+ Type: issues_model.PullRequestGitea,
+ }
+ err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
+ assert.NoError(t, err)
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
+ conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
+ assert.NoError(t, err)
+
+ // Ensure conflictedFiles is populated.
+ assert.Equal(t, 1, len(conflictingPR.ConflictedFiles))
+ // Check if status is correct.
+ assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
+ // Ensure that mergeable returns false
+ assert.False(t, conflictingPR.Mergeable())
+ })
+}
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
new file mode 100644
index 0000000000..d713c0f858
--- /dev/null
+++ b/tests/integration/pull_review_test.go
@@ -0,0 +1,22 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+)
+
+func TestPullView_ReviewerMissed(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+
+ req := NewRequest(t, "GET", "/pulls")
+ session.MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
+ session.MakeRequest(t, req, http.StatusOK)
+}
diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go
new file mode 100644
index 0000000000..0f9cd41ec2
--- /dev/null
+++ b/tests/integration/pull_status_test.go
@@ -0,0 +1,157 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "path"
+ "strings"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPullCreate_CommitStatus(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
+
+ url := path.Join("user1", "repo1", "compare", "master...status1")
+ req := NewRequestWithValues(t, "POST", url,
+ map[string]string{
+ "_csrf": GetCSRF(t, session, url),
+ "title": "pull request from status1",
+ },
+ )
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequest(t, "GET", "/user1/repo1/pulls")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ NewHTMLParser(t, resp.Body)
+
+ // Request repository commits page
+ req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+
+ // Get first commit URL
+ commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
+ assert.True(t, exists)
+ assert.NotEmpty(t, commitURL)
+
+ commitID := path.Base(commitURL)
+
+ statusList := []api.CommitStatusState{
+ api.CommitStatusPending,
+ api.CommitStatusError,
+ api.CommitStatusFailure,
+ api.CommitStatusWarning,
+ api.CommitStatusSuccess,
+ }
+
+ statesIcons := map[api.CommitStatusState]string{
+ api.CommitStatusPending: "octicon-dot-fill",
+ api.CommitStatusSuccess: "octicon-check",
+ api.CommitStatusError: "gitea-exclamation",
+ api.CommitStatusFailure: "octicon-x",
+ api.CommitStatusWarning: "gitea-exclamation",
+ }
+
+ testCtx := NewAPITestContext(t, "user1", "repo1")
+
+ // Update commit status, and check if icon is updated as well
+ for _, status := range statusList {
+
+ // Call API to add status for commit
+ t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, status))
+
+ req = NewRequestf(t, "GET", "/user1/repo1/pulls/1/commits")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ doc = NewHTMLParser(t, resp.Body)
+
+ commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
+ assert.True(t, exists)
+ assert.NotEmpty(t, commitURL)
+ assert.EqualValues(t, commitID, path.Base(commitURL))
+
+ cls, ok := doc.doc.Find("#commits-table tbody tr td.message .commit-status").Last().Attr("class")
+ assert.True(t, ok)
+ assert.Contains(t, cls, statesIcons[status])
+ }
+ })
+}
+
+func doAPICreateCommitStatus(ctx APITestContext, commitID string, status api.CommitStatusState) func(*testing.T) {
+ return func(t *testing.T) {
+ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s?token=%s", ctx.Username, ctx.Reponame, commitID, ctx.Token),
+ api.CreateStatusOption{
+ State: status,
+ TargetURL: "http://test.ci/",
+ Description: "",
+ Context: "testci",
+ },
+ )
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ ctx.Session.MakeRequest(t, req, http.StatusCreated)
+ }
+}
+
+func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
+ // Merge must continue if commits SHA are different, even if content is same
+ // Reason: gitflow and merging master back into develop, where is high possiblity, there are no changes
+ // but just commit saying "Merge branch". And this meta commit can be also tagged,
+ // so we need to have this meta commit also in develop branch.
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
+ testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1")
+
+ url := path.Join("user1", "repo1", "compare", "master...status1")
+ req := NewRequestWithValues(t, "POST", url,
+ map[string]string{
+ "_csrf": GetCSRF(t, session, url),
+ "title": "pull request from status1",
+ },
+ )
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequest(t, "GET", "/user1/repo1/pulls/1")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+
+ text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
+ assert.Contains(t, text, "This pull request can be merged automatically.")
+ })
+}
+
+func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther)
+ url := path.Join("user1", "repo1", "compare", "master...status1")
+ req := NewRequestWithValues(t, "POST", url,
+ map[string]string{
+ "_csrf": GetCSRF(t, session, url),
+ "title": "pull request from status1",
+ },
+ )
+ session.MakeRequest(t, req, http.StatusSeeOther)
+ req = NewRequest(t, "GET", "/user1/repo1/pulls/1")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+
+ text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
+ assert.Contains(t, text, "This branch is already included in the target branch. There is nothing to merge.")
+ })
+}
diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go
new file mode 100644
index 0000000000..c08faaaeb6
--- /dev/null
+++ b/tests/integration/pull_update_test.go
@@ -0,0 +1,174 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ pull_service "code.gitea.io/gitea/services/pull"
+ repo_service "code.gitea.io/gitea/services/repository"
+ files_service "code.gitea.io/gitea/services/repository/files"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIPullUpdate(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ // Create PR to test
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
+ pr := createOutdatedPR(t, user, org26)
+
+ // Test GetDiverging
+ diffCount, err := pull_service.GetDiverging(git.DefaultContext, pr)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 1, diffCount.Behind)
+ assert.EqualValues(t, 1, diffCount.Ahead)
+ assert.NoError(t, pr.LoadBaseRepo())
+ assert.NoError(t, pr.LoadIssue())
+
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?token="+token, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test GetDiverging after update
+ diffCount, err = pull_service.GetDiverging(git.DefaultContext, pr)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 0, diffCount.Behind)
+ assert.EqualValues(t, 2, diffCount.Ahead)
+ })
+}
+
+func TestAPIPullUpdateByRebase(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ // Create PR to test
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
+ pr := createOutdatedPR(t, user, org26)
+
+ // Test GetDiverging
+ diffCount, err := pull_service.GetDiverging(git.DefaultContext, pr)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 1, diffCount.Behind)
+ assert.EqualValues(t, 1, diffCount.Ahead)
+ assert.NoError(t, pr.LoadBaseRepo())
+ assert.NoError(t, pr.LoadIssue())
+
+ session := loginUser(t, "user2")
+ token := getTokenForLoggedInUser(t, session)
+ req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase&token="+token, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // Test GetDiverging after update
+ diffCount, err = pull_service.GetDiverging(git.DefaultContext, pr)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 0, diffCount.Behind)
+ assert.EqualValues(t, 1, diffCount.Ahead)
+ })
+}
+
+func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
+ baseRepo, err := repo_service.CreateRepository(actor, actor, repo_module.CreateRepoOptions{
+ Name: "repo-pr-update",
+ Description: "repo-tmp-pr-update description",
+ AutoInit: true,
+ Gitignores: "C,C++",
+ License: "MIT",
+ Readme: "Default",
+ IsPrivate: false,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, baseRepo)
+
+ headRepo, err := repo_service.ForkRepository(git.DefaultContext, actor, forkOrg, repo_service.ForkRepoOptions{
+ BaseRepo: baseRepo,
+ Name: "repo-pr-update",
+ Description: "desc",
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, headRepo)
+
+ // create a commit on base Repo
+ _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, actor, &files_service.UpdateRepoFileOptions{
+ TreePath: "File_A",
+ Message: "Add File A",
+ Content: "File A",
+ IsNewFile: true,
+ OldBranch: "master",
+ NewBranch: "master",
+ Author: &files_service.IdentityOptions{
+ Name: actor.Name,
+ Email: actor.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ Name: actor.Name,
+ Email: actor.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+
+ // create a commit on head Repo
+ _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, headRepo, actor, &files_service.UpdateRepoFileOptions{
+ TreePath: "File_B",
+ Message: "Add File on PR branch",
+ Content: "File B",
+ IsNewFile: true,
+ OldBranch: "master",
+ NewBranch: "newBranch",
+ Author: &files_service.IdentityOptions{
+ Name: actor.Name,
+ Email: actor.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ Name: actor.Name,
+ Email: actor.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+
+ // create Pull
+ pullIssue := &issues_model.Issue{
+ RepoID: baseRepo.ID,
+ Title: "Test Pull -to-update-",
+ PosterID: actor.ID,
+ Poster: actor,
+ IsPull: true,
+ }
+ pullRequest := &issues_model.PullRequest{
+ HeadRepoID: headRepo.ID,
+ BaseRepoID: baseRepo.ID,
+ HeadBranch: "newBranch",
+ BaseBranch: "master",
+ HeadRepo: headRepo,
+ BaseRepo: baseRepo,
+ Type: issues_model.PullRequestGitea,
+ }
+ err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
+ assert.NoError(t, err)
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"})
+ pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
+ assert.NoError(t, err)
+
+ return pr
+}
diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go
new file mode 100644
index 0000000000..2a52a5cde2
--- /dev/null
+++ b/tests/integration/release_test.go
@@ -0,0 +1,213 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/stretchr/testify/assert"
+)
+
+func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title string, preRelease, draft bool) {
+ req := NewRequest(t, "GET", repoURL+"/releases/new")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
+ assert.True(t, exists, "The template has changed")
+
+ postData := map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "tag_name": tag,
+ "tag_target": "master",
+ "title": title,
+ "content": "",
+ }
+ if preRelease {
+ postData["prerelease"] = "on"
+ }
+ if draft {
+ postData["draft"] = "Save Draft"
+ }
+ req = NewRequestWithValues(t, "POST", link, postData)
+
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ test.RedirectURL(resp) // check that redirect URL exists
+}
+
+func checkLatestReleaseAndCount(t *testing.T, session *TestSession, repoURL, version, label string, count int) {
+ req := NewRequest(t, "GET", repoURL+"/releases")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ labelText := htmlDoc.doc.Find("#release-list > li .meta .label").First().Text()
+ assert.EqualValues(t, label, labelText)
+ titleText := htmlDoc.doc.Find("#release-list > li .detail h4 a").First().Text()
+ assert.EqualValues(t, version, titleText)
+
+ releaseList := htmlDoc.doc.Find("#release-list > li")
+ assert.EqualValues(t, count, releaseList.Length())
+}
+
+func TestViewReleases(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/user2/repo1/releases")
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // if CI is to slow this test fail, so lets wait a bit
+ time.Sleep(time.Millisecond * 100)
+}
+
+func TestViewReleasesNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/releases")
+ MakeRequest(t, req, http.StatusOK)
+}
+
+func TestCreateRelease(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false)
+
+ checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4)
+}
+
+func TestCreateReleasePreRelease(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false)
+
+ checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.prerelease"), 4)
+}
+
+func TestCreateReleaseDraft(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true)
+
+ checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.draft"), 4)
+}
+
+func TestCreateReleasePaging(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ oldAPIDefaultNum := setting.API.DefaultPagingNum
+ defer func() {
+ setting.API.DefaultPagingNum = oldAPIDefaultNum
+ }()
+ setting.API.DefaultPagingNum = 10
+
+ session := loginUser(t, "user2")
+ // Create enough releases to have paging
+ for i := 0; i < 12; i++ {
+ version := fmt.Sprintf("v0.0.%d", i)
+ createNewRelease(t, session, "/user2/repo1", version, version, false, false)
+ }
+ createNewRelease(t, session, "/user2/repo1", "v0.0.12", "v0.0.12", false, true)
+
+ checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").Tr("repo.release.draft"), 10)
+
+ // Check that user4 does not see draft and still see 10 latest releases
+ session2 := loginUser(t, "user4")
+ checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").Tr("repo.release.stable"), 10)
+}
+
+func TestViewReleaseListNoLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ link := repo.Link() + "/releases"
+
+ req := NewRequest(t, "GET", link)
+ rsp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, rsp.Body)
+ releases := htmlDoc.Find("#release-list li.ui.grid")
+ assert.Equal(t, 2, releases.Length())
+
+ links := make([]string, 0, 5)
+ releases.Each(func(i int, s *goquery.Selection) {
+ link, exist := s.Find(".release-list-title a").Attr("href")
+ if !exist {
+ return
+ }
+ links = append(links, link)
+ })
+
+ assert.EqualValues(t, []string{"/user2/repo1/releases/tag/v1.0", "/user2/repo1/releases/tag/v1.1"}, links)
+}
+
+func TestViewReleaseListLogin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ link := repo.Link() + "/releases"
+
+ session := loginUser(t, "user1")
+ req := NewRequest(t, "GET", link)
+ rsp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, rsp.Body)
+ releases := htmlDoc.Find("#release-list li.ui.grid")
+ assert.Equal(t, 3, releases.Length())
+
+ links := make([]string, 0, 5)
+ releases.Each(func(i int, s *goquery.Selection) {
+ link, exist := s.Find(".release-list-title a").Attr("href")
+ if !exist {
+ return
+ }
+ links = append(links, link)
+ })
+
+ assert.EqualValues(t, []string{
+ "/user2/repo1/releases/tag/draft-release",
+ "/user2/repo1/releases/tag/v1.0",
+ "/user2/repo1/releases/tag/v1.1",
+ }, links)
+}
+
+func TestViewTagsList(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ link := repo.Link() + "/tags"
+
+ session := loginUser(t, "user1")
+ req := NewRequest(t, "GET", link)
+ rsp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, rsp.Body)
+ tags := htmlDoc.Find(".tag-list tr")
+ assert.Equal(t, 3, tags.Length())
+
+ tagNames := make([]string, 0, 5)
+ tags.Each(func(i int, s *goquery.Selection) {
+ tagNames = append(tagNames, s.Find(".tag a.df.ac").Text())
+ })
+
+ assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames)
+}
diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go
new file mode 100644
index 0000000000..9ea69702af
--- /dev/null
+++ b/tests/integration/rename_branch_test.go
@@ -0,0 +1,45 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenameBranch(t *testing.T) {
+ // get branch setting page
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/user2/repo1/settings/branches")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ postData := map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "from": "master",
+ "to": "main",
+ }
+ req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", postData)
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // check new branch link
+ req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", postData)
+ session.MakeRequest(t, req, http.StatusOK)
+
+ // check old branch link
+ req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", postData)
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+ location := resp.HeaderMap.Get("Location")
+ assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location)
+
+ // check db
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.Equal(t, "main", repo1.DefaultBranch)
+}
diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go
new file mode 100644
index 0000000000..ea8845ac39
--- /dev/null
+++ b/tests/integration/repo_activity_test.go
@@ -0,0 +1,66 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/test"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepoActivity(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ session := loginUser(t, "user1")
+
+ // Create PRs (1 merged & 2 proposed)
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+ testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
+ resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
+ elem := strings.Split(test.RedirectURL(resp), "/")
+ assert.EqualValues(t, "pulls", elem[3])
+ testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
+
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/better_readme", "README.md", "Hello, World (Edited Again)\n")
+ testPullCreate(t, session, "user1", "repo1", "feat/better_readme", "This is a pull title")
+
+ testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feat/much_better_readme", "README.md", "Hello, World (Edited More)\n")
+ testPullCreate(t, session, "user1", "repo1", "feat/much_better_readme", "This is a pull title")
+
+ // Create issues (3 new issues)
+ testNewIssue(t, session, "user2", "repo1", "Issue 1", "Description 1")
+ testNewIssue(t, session, "user2", "repo1", "Issue 2", "Description 2")
+ testNewIssue(t, session, "user2", "repo1", "Issue 3", "Description 3")
+
+ // Create releases (1 new release)
+ createNewRelease(t, session, "/user2/repo1", "v1.0.0", "v1.0.0", false, false)
+
+ // Open Activity page and check stats
+ req := NewRequest(t, "GET", "/user2/repo1/activity")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ // Should be 1 published release
+ list := htmlDoc.doc.Find("#published-releases").Next().Find("p.desc")
+ assert.Len(t, list.Nodes, 1)
+
+ // Should be 1 merged pull request
+ list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc")
+ assert.Len(t, list.Nodes, 1)
+
+ // Should be 2 proposed pull requests
+ list = htmlDoc.doc.Find("#proposed-pull-requests").Next().Find("p.desc")
+ assert.Len(t, list.Nodes, 2)
+
+ // Should be 3 new issues
+ list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc")
+ assert.Len(t, list.Nodes, 3)
+ })
+}
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
new file mode 100644
index 0000000000..96ffa5a46e
--- /dev/null
+++ b/tests/integration/repo_branch_test.go
@@ -0,0 +1,148 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "path"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string {
+ var csrf string
+ if expectedStatus == http.StatusNotFound {
+ csrf = GetCSRF(t, session, path.Join(user, repo, "src/branch/master"))
+ } else {
+ csrf = GetCSRF(t, session, path.Join(user, repo, "src", oldRefSubURL))
+ }
+ req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefSubURL), map[string]string{
+ "_csrf": csrf,
+ "new_branch_name": newBranchName,
+ })
+ resp := session.MakeRequest(t, req, expectedStatus)
+ if expectedStatus != http.StatusSeeOther {
+ return ""
+ }
+ return test.RedirectURL(resp)
+}
+
+func TestCreateBranch(t *testing.T) {
+ onGiteaRun(t, testCreateBranches)
+}
+
+func testCreateBranches(t *testing.T, giteaURL *url.URL) {
+ tests := []struct {
+ OldRefSubURL string
+ NewBranch string
+ CreateRelease string
+ FlashMessage string
+ ExpectedStatus int
+ }{
+ {
+ OldRefSubURL: "branch/master",
+ NewBranch: "feature/test1",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"),
+ },
+ {
+ OldRefSubURL: "branch/master",
+ NewBranch: "",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.require_error"),
+ },
+ {
+ OldRefSubURL: "branch/master",
+ NewBranch: "feature=test1",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"),
+ },
+ {
+ OldRefSubURL: "branch/master",
+ NewBranch: strings.Repeat("b", 101),
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.max_size_error", "100"),
+ },
+ {
+ OldRefSubURL: "branch/master",
+ NewBranch: "master",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.branch_already_exists", "master"),
+ },
+ {
+ OldRefSubURL: "branch/master",
+ NewBranch: "master/test",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.branch_name_conflict", "master/test", "master"),
+ },
+ {
+ OldRefSubURL: "commit/acd1d892867872cb47f3993468605b8aa59aa2e0",
+ NewBranch: "feature/test2",
+ ExpectedStatus: http.StatusNotFound,
+ },
+ {
+ OldRefSubURL: "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ NewBranch: "feature/test3",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"),
+ },
+ {
+ OldRefSubURL: "branch/master",
+ NewBranch: "v1.0.0",
+ CreateRelease: "v1.0.0",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.tag_collision", "v1.0.0"),
+ },
+ {
+ OldRefSubURL: "tag/v1.0.0",
+ NewBranch: "feature/test4",
+ CreateRelease: "v1.0.1",
+ ExpectedStatus: http.StatusSeeOther,
+ FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"),
+ },
+ }
+ for _, test := range tests {
+ session := loginUser(t, "user2")
+ if test.CreateRelease != "" {
+ createNewRelease(t, session, "/user2/repo1", test.CreateRelease, test.CreateRelease, false, false)
+ }
+ redirectURL := testCreateBranch(t, session, "user2", "repo1", test.OldRefSubURL, test.NewBranch, test.ExpectedStatus)
+ if test.ExpectedStatus == http.StatusSeeOther {
+ req := NewRequest(t, "GET", redirectURL)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ test.FlashMessage,
+ strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
+ )
+ }
+ }
+}
+
+func TestCreateBranchInvalidCSRF(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ req := NewRequestWithValues(t, "POST", "user2/repo1/branches/_new/branch/master", map[string]string{
+ "_csrf": "fake_csrf",
+ "new_branch_name": "test",
+ })
+ resp := session.MakeRequest(t, req, http.StatusSeeOther)
+ loc := resp.Header().Get("Location")
+ assert.Equal(t, setting.AppSubURL+"/", loc)
+ resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ "Bad Request: invalid CSRF token",
+ strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
+ )
+}
diff --git a/tests/integration/repo_commits_search_test.go b/tests/integration/repo_commits_search_test.go
new file mode 100644
index 0000000000..75e692f0ab
--- /dev/null
+++ b/tests/integration/repo_commits_search_test.go
@@ -0,0 +1,43 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func testRepoCommitsSearch(t *testing.T, query, commit string) {
+ session := loginUser(t, "user2")
+
+ // Request repository commits page
+ req := NewRequestf(t, "GET", "/user2/commits_search_test/commits/branch/master/search?q=%s", url.QueryEscape(query))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ sel := doc.doc.Find("#commits-table tbody tr td.sha a")
+ assert.EqualValues(t, commit, strings.TrimSpace(sel.Text()))
+}
+
+func TestRepoCommitsSearch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ testRepoCommitsSearch(t, "e8eabd", "")
+ testRepoCommitsSearch(t, "38a9cb", "")
+ testRepoCommitsSearch(t, "6e8e", "6e8eabd9a7")
+ testRepoCommitsSearch(t, "58e97", "58e97d1a24")
+ testRepoCommitsSearch(t, "author:alice", "6e8eabd9a7")
+ testRepoCommitsSearch(t, "author:alice 6e8ea", "6e8eabd9a7")
+ testRepoCommitsSearch(t, "committer:Tom", "58e97d1a24")
+ testRepoCommitsSearch(t, "author:bob commit-4", "58e97d1a24")
+ testRepoCommitsSearch(t, "author:bob commit after:2019-03-03", "58e97d1a24")
+ testRepoCommitsSearch(t, "committer:alice 6e8e before:2019-03-02", "6e8eabd9a7")
+ testRepoCommitsSearch(t, "committer:alice commit before:2019-03-02", "6e8eabd9a7")
+ testRepoCommitsSearch(t, "committer:alice author:tom commit before:2019-03-04 after:2019-03-02", "0a8499a22a")
+}
diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go
new file mode 100644
index 0000000000..c9e7753596
--- /dev/null
+++ b/tests/integration/repo_commits_test.go
@@ -0,0 +1,117 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "path"
+ "testing"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepoCommits(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ // Request repository commits page
+ req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href")
+ assert.True(t, exists)
+ assert.NotEmpty(t, commitURL)
+}
+
+func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ // Request repository commits page
+ req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ // Get first commit URL
+ commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href")
+ assert.True(t, exists)
+ assert.NotEmpty(t, commitURL)
+
+ // Call API to add status for commit
+ t.Run("CreateStatus", doAPICreateCommitStatus(NewAPITestContext(t, "user2", "repo1"), path.Base(commitURL), api.CommitStatusState(state)))
+
+ req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ doc = NewHTMLParser(t, resp.Body)
+ // Check if commit status is displayed in message column
+ sel := doc.doc.Find("#commits-table tbody tr td.message a.commit-statuses-trigger .commit-status")
+ assert.Equal(t, 1, sel.Length())
+ for _, class := range classes {
+ assert.True(t, sel.HasClass(class))
+ }
+
+ // By SHA
+ req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/"+path.Base(commitURL)+"/statuses")
+ reqOne := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/"+path.Base(commitURL)+"/status")
+ testRepoCommitsWithStatus(t, session.MakeRequest(t, req, http.StatusOK), session.MakeRequest(t, reqOne, http.StatusOK), state)
+
+ // By Ref
+ req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/master/statuses")
+ reqOne = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/master/status")
+ testRepoCommitsWithStatus(t, session.MakeRequest(t, req, http.StatusOK), session.MakeRequest(t, reqOne, http.StatusOK), state)
+ req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/v1.1/statuses")
+ reqOne = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/commits/v1.1/status")
+ testRepoCommitsWithStatus(t, session.MakeRequest(t, req, http.StatusOK), session.MakeRequest(t, reqOne, http.StatusOK), state)
+}
+
+func testRepoCommitsWithStatus(t *testing.T, resp, respOne *httptest.ResponseRecorder, state string) {
+ var statuses []*api.CommitStatus
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &statuses))
+ var status api.CombinedStatus
+ assert.NoError(t, json.Unmarshal(respOne.Body.Bytes(), &status))
+ assert.NotNil(t, status)
+
+ if assert.Len(t, statuses, 1) {
+ assert.Equal(t, api.CommitStatusState(state), statuses[0].State)
+ assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/statuses/65f1bf27bc3bf70f64657658635e66094edbcb4d", statuses[0].URL)
+ assert.Equal(t, "http://test.ci/", statuses[0].TargetURL)
+ assert.Equal(t, "", statuses[0].Description)
+ assert.Equal(t, "testci", statuses[0].Context)
+
+ assert.Len(t, status.Statuses, 1)
+ assert.Equal(t, statuses[0], status.Statuses[0])
+ assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", status.SHA)
+ }
+}
+
+func TestRepoCommitsWithStatusPending(t *testing.T) {
+ doTestRepoCommitWithStatus(t, "pending", "octicon-dot-fill", "yellow")
+}
+
+func TestRepoCommitsWithStatusSuccess(t *testing.T) {
+ doTestRepoCommitWithStatus(t, "success", "octicon-check", "green")
+}
+
+func TestRepoCommitsWithStatusError(t *testing.T) {
+ doTestRepoCommitWithStatus(t, "error", "gitea-exclamation", "red")
+}
+
+func TestRepoCommitsWithStatusFailure(t *testing.T) {
+ doTestRepoCommitWithStatus(t, "failure", "octicon-x", "red")
+}
+
+func TestRepoCommitsWithStatusWarning(t *testing.T) {
+ doTestRepoCommitWithStatus(t, "warning", "gitea-exclamation", "yellow")
+}
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
new file mode 100644
index 0000000000..4ab3577b54
--- /dev/null
+++ b/tests/integration/repo_fork_test.go
@@ -0,0 +1,76 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName string) *httptest.ResponseRecorder {
+ forkOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: forkOwnerName})
+
+ // Step0: check the existence of the to-fork repo
+ req := NewRequestf(t, "GET", "/%s/%s", forkOwnerName, forkRepoName)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Step1: go to the main page of repo
+ req = NewRequestf(t, "GET", "/%s/%s", ownerName, repoName)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ // Step2: click the fork button
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href")
+ assert.True(t, exists, "The template has changed")
+ req = NewRequest(t, "GET", link)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ // Step3: fill the form of the forking
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/fork/\"]").Attr("action")
+ assert.True(t, exists, "The template has changed")
+ _, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value")
+ assert.True(t, exists, fmt.Sprintf("Fork owner '%s' is not present in select box", forkOwnerName))
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "uid": fmt.Sprintf("%d", forkOwner.ID),
+ "repo_name": forkRepoName,
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // Step4: check the existence of the forked repo
+ req = NewRequestf(t, "GET", "/%s/%s", forkOwnerName, forkRepoName)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ return resp
+}
+
+func TestRepoFork(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+ testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+}
+
+func TestRepoForkToOrg(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ testRepoFork(t, session, "user2", "repo1", "user3", "repo1")
+
+ // Check that no more forking is allowed as user2 owns repository
+ // and user3 organization that owner user2 is also now has forked this repository
+ req := NewRequest(t, "GET", "/user2/repo1")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ _, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href")
+ assert.False(t, exists, "Forking should not be allowed anymore")
+}
diff --git a/tests/integration/repo_generate_test.go b/tests/integration/repo_generate_test.go
new file mode 100644
index 0000000000..61a632721e
--- /dev/null
+++ b/tests/integration/repo_generate_test.go
@@ -0,0 +1,69 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testRepoGenerate(t *testing.T, session *TestSession, templateOwnerName, templateRepoName, generateOwnerName, generateRepoName string) *httptest.ResponseRecorder {
+ generateOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: generateOwnerName})
+
+ // Step0: check the existence of the generated repo
+ req := NewRequestf(t, "GET", "/%s/%s", generateOwnerName, generateRepoName)
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // Step1: go to the main page of template repo
+ req = NewRequestf(t, "GET", "/%s/%s", templateOwnerName, templateRepoName)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ // Step2: click the "Use this template" button
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/create\"]").Attr("href")
+ assert.True(t, exists, "The template has changed")
+ req = NewRequest(t, "GET", link)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ // Step3: fill the form of the create
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/create\"]").Attr("action")
+ assert.True(t, exists, "The template has changed")
+ _, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value")
+ assert.True(t, exists, fmt.Sprintf("Generate owner '%s' is not present in select box", generateOwnerName))
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "uid": fmt.Sprintf("%d", generateOwner.ID),
+ "repo_name": generateRepoName,
+ "git_content": "true",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // Step4: check the existence of the generated repo
+ req = NewRequestf(t, "GET", "/%s/%s", generateOwnerName, generateRepoName)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ return resp
+}
+
+func TestRepoGenerate(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+ testRepoGenerate(t, session, "user27", "template1", "user1", "generated1")
+}
+
+func TestRepoGenerateToOrg(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ testRepoGenerate(t, session, "user27", "template1", "user2", "generated2")
+}
diff --git a/tests/integration/repo_migrate_test.go b/tests/integration/repo_migrate_test.go
new file mode 100644
index 0000000000..c69a2642cb
--- /dev/null
+++ b/tests/integration/repo_migrate_test.go
@@ -0,0 +1,46 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *httptest.ResponseRecorder {
+ req := NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", structs.PlainGitService)) // render plain git migration page
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
+ assert.True(t, exists, "The template has changed")
+
+ uid, exists := htmlDoc.doc.Find("#uid").Attr("value")
+ assert.True(t, exists, "The template has changed")
+
+ req = NewRequestWithValues(t, "POST", link, map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ "clone_addr": cloneAddr,
+ "uid": uid,
+ "repo_name": repoName,
+ "service": fmt.Sprintf("%d", structs.PlainGitService),
+ })
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ return resp
+}
+
+func TestRepoMigrate(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ testRepoMigrate(t, session, "https://github.com/go-gitea/test_repo.git", "git")
+}
diff --git a/tests/integration/repo_search_test.go b/tests/integration/repo_search_test.go
new file mode 100644
index 0000000000..b20943c22a
--- /dev/null
+++ b/tests/integration/repo_search_test.go
@@ -0,0 +1,63 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ code_indexer "code.gitea.io/gitea/modules/indexer/code"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/stretchr/testify/assert"
+)
+
+func resultFilenames(t testing.TB, doc *HTMLDoc) []string {
+ filenameSelections := doc.doc.Find(".repository.search").Find(".repo-search-result").Find(".header").Find("span.file")
+ result := make([]string, filenameSelections.Length())
+ filenameSelections.Each(func(i int, selection *goquery.Selection) {
+ result[i] = selection.Text()
+ })
+ return result
+}
+
+func TestSearchRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo, err := repo_model.GetRepositoryByOwnerAndName("user2", "repo1")
+ assert.NoError(t, err)
+
+ executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
+
+ testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"})
+
+ setting.Indexer.IncludePatterns = setting.IndexerGlobFromString("**.txt")
+ setting.Indexer.ExcludePatterns = setting.IndexerGlobFromString("**/y/**")
+
+ repo, err = repo_model.GetRepositoryByOwnerAndName("user2", "glob")
+ assert.NoError(t, err)
+
+ executeIndexer(t, repo, code_indexer.UpdateRepoIndexer)
+
+ testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"})
+ testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt"})
+ testSearch(t, "/user2/glob/search?q=file4&page=1", []string{})
+ testSearch(t, "/user2/glob/search?q=file5&page=1", []string{})
+}
+
+func testSearch(t *testing.T, url string, expected []string) {
+ req := NewRequestf(t, "GET", url)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ filenames := resultFilenames(t, NewHTMLParser(t, resp.Body))
+ assert.EqualValues(t, expected, filenames)
+}
+
+func executeIndexer(t *testing.T, repo *repo_model.Repository, op func(*repo_model.Repository)) {
+ op(repo)
+}
diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go
new file mode 100644
index 0000000000..a91f1fb209
--- /dev/null
+++ b/tests/integration/repo_tag_test.go
@@ -0,0 +1,99 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/url"
+ "os"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/release"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateNewTagProtected(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ t.Run("API", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ err := release.CreateNewTag(git.DefaultContext, owner, repo, "master", "v-1", "first tag")
+ assert.NoError(t, err)
+
+ err = git_model.InsertProtectedTag(&git_model.ProtectedTag{
+ RepoID: repo.ID,
+ NamePattern: "v-*",
+ })
+ assert.NoError(t, err)
+ err = git_model.InsertProtectedTag(&git_model.ProtectedTag{
+ RepoID: repo.ID,
+ NamePattern: "v-1.1",
+ AllowlistUserIDs: []int64{repo.OwnerID},
+ })
+ assert.NoError(t, err)
+
+ err = release.CreateNewTag(git.DefaultContext, owner, repo, "master", "v-2", "second tag")
+ assert.Error(t, err)
+ assert.True(t, models.IsErrProtectedTagName(err))
+
+ err = release.CreateNewTag(git.DefaultContext, owner, repo, "master", "v-1.1", "third tag")
+ assert.NoError(t, err)
+ })
+
+ t.Run("Git", func(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ username := "user2"
+ httpContext := NewAPITestContext(t, username, "repo1")
+
+ dstPath, err := os.MkdirTemp("", httpContext.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ u.Path = httpContext.GitPath()
+ u.User = url.UserPassword(username, userPassword)
+
+ doGitClone(dstPath, u)(t)
+
+ _, _, err = git.NewCommand(git.DefaultContext, "tag", "v-2").RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.NoError(t, err)
+
+ _, _, err = git.NewCommand(git.DefaultContext, "push", "--tags").RunStdString(&git.RunOpts{Dir: dstPath})
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "Tag v-2 is protected")
+ })
+ })
+
+ // Cleanup
+ releases, err := repo_model.GetReleasesByRepoID(repo.ID, repo_model.FindReleasesOptions{
+ IncludeTags: true,
+ TagNames: []string{"v-1", "v-1.1"},
+ })
+ assert.NoError(t, err)
+
+ for _, release := range releases {
+ err = repo_model.DeleteReleaseByID(release.ID)
+ assert.NoError(t, err)
+ }
+
+ protectedTags, err := git_model.GetProtectedTags(repo.ID)
+ assert.NoError(t, err)
+
+ for _, protectedTag := range protectedTags {
+ err = git_model.DeleteProtectedTag(protectedTag)
+ assert.NoError(t, err)
+ }
+}
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
new file mode 100644
index 0000000000..8dfa9d08f1
--- /dev/null
+++ b/tests/integration/repo_test.go
@@ -0,0 +1,184 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "path"
+ "strings"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/PuerkitoBio/goquery"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViewRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1")
+ MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequest(t, "GET", "/user3/repo3")
+ MakeRequest(t, req, http.StatusNotFound)
+
+ session := loginUser(t, "user1")
+ session.MakeRequest(t, req, http.StatusNotFound)
+}
+
+func testViewRepo(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user3/repo3")
+ session := loginUser(t, "user2")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ files := htmlDoc.doc.Find("#repo-files-table > TBODY > TR")
+
+ type file struct {
+ fileName string
+ commitID string
+ commitMsg string
+ commitTime string
+ }
+
+ var items []file
+
+ files.Each(func(i int, s *goquery.Selection) {
+ tds := s.Find("td")
+ var f file
+ tds.Each(func(i int, s *goquery.Selection) {
+ if i == 0 {
+ f.fileName = strings.TrimSpace(s.Text())
+ } else if i == 1 {
+ a := s.Find("a")
+ f.commitMsg = strings.TrimSpace(a.Text())
+ l, _ := a.Attr("href")
+ f.commitID = path.Base(l)
+ }
+ })
+
+ f.commitTime, _ = s.Find("span.time-since").Attr("data-content")
+ items = append(items, f)
+ })
+
+ commitT := time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).In(time.Local).Format(time.RFC1123)
+ assert.EqualValues(t, []file{
+ {
+ fileName: "doc",
+ commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
+ commitMsg: "init project",
+ commitTime: commitT,
+ },
+ {
+ fileName: "README.md",
+ commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
+ commitMsg: "init project",
+ commitTime: commitT,
+ },
+ }, items)
+}
+
+func TestViewRepo2(t *testing.T) {
+ // no last commit cache
+ testViewRepo(t)
+
+ // enable last commit cache for all repositories
+ oldCommitsCount := setting.CacheService.LastCommit.CommitsCount
+ setting.CacheService.LastCommit.CommitsCount = 0
+ // first view will not hit the cache
+ testViewRepo(t)
+ // second view will hit the cache
+ testViewRepo(t)
+ setting.CacheService.LastCommit.CommitsCount = oldCommitsCount
+}
+
+func TestViewRepo3(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user3/repo3")
+ session := loginUser(t, "user4")
+ session.MakeRequest(t, req, http.StatusOK)
+}
+
+func TestViewRepo1CloneLinkAnonymous(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user2/repo1")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find("#repo-clone-https").Attr("data-link")
+ assert.True(t, exists, "The template has changed")
+ assert.Equal(t, setting.AppURL+"user2/repo1.git", link)
+ _, exists = htmlDoc.doc.Find("#repo-clone-ssh").Attr("data-link")
+ assert.False(t, exists)
+}
+
+func TestViewRepo1CloneLinkAuthorized(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ req := NewRequest(t, "GET", "/user2/repo1")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ link, exists := htmlDoc.doc.Find("#repo-clone-https").Attr("data-link")
+ assert.True(t, exists, "The template has changed")
+ assert.Equal(t, setting.AppURL+"user2/repo1.git", link)
+ link, exists = htmlDoc.doc.Find("#repo-clone-ssh").Attr("data-link")
+ assert.True(t, exists, "The template has changed")
+ sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port)
+ assert.Equal(t, sshURL, link)
+}
+
+func TestViewRepoWithSymlinks(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ req := NewRequest(t, "GET", "/user2/repo20.git")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ files := htmlDoc.doc.Find("#repo-files-table > TBODY > TR > TD.name > SPAN.truncate")
+ items := files.Map(func(i int, s *goquery.Selection) string {
+ cls, _ := s.Find("SVG").Attr("class")
+ file := strings.Trim(s.Find("A").Text(), " \t\n")
+ return fmt.Sprintf("%s: %s", file, cls)
+ })
+ assert.Len(t, items, 5)
+ assert.Equal(t, "a: svg octicon-file-directory-fill", items[0])
+ assert.Equal(t, "link_b: svg octicon-file-submodule", items[1])
+ assert.Equal(t, "link_d: svg octicon-file-symlink-file", items[2])
+ assert.Equal(t, "link_hi: svg octicon-file-symlink-file", items[3])
+ assert.Equal(t, "link_link: svg octicon-file-symlink-file", items[4])
+}
+
+// TestViewAsRepoAdmin tests PR #2167
+func TestViewAsRepoAdmin(t *testing.T) {
+ for user, expectedNoDescription := range map[string]bool{
+ "user2": true,
+ "user4": false,
+ } {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, user)
+
+ req := NewRequest(t, "GET", "/user2/repo1.git")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ noDescription := htmlDoc.doc.Find("#repo-desc").Children()
+
+ assert.Equal(t, expectedNoDescription, noDescription.HasClass("no-description"))
+ }
+}
diff --git a/tests/integration/repo_topic_test.go b/tests/integration/repo_topic_test.go
new file mode 100644
index 0000000000..5ff0c8273a
--- /dev/null
+++ b/tests/integration/repo_topic_test.go
@@ -0,0 +1,48 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTopicSearch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ searchURL, _ := url.Parse("/explore/topics/search")
+ var topics struct {
+ TopicNames []*api.TopicResponse `json:"topics"`
+ }
+
+ query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
+
+ searchURL.RawQuery = query.Encode()
+ res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 4)
+ assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+ query.Add("q", "topic")
+ searchURL.RawQuery = query.Encode()
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 2)
+
+ query.Set("q", "database")
+ searchURL.RawQuery = query.Encode()
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ if assert.Len(t, topics.TopicNames, 1) {
+ assert.EqualValues(t, 2, topics.TopicNames[0].ID)
+ assert.EqualValues(t, "database", topics.TopicNames[0].Name)
+ assert.EqualValues(t, 1, topics.TopicNames[0].RepoCount)
+ }
+}
diff --git a/tests/integration/repo_watch_test.go b/tests/integration/repo_watch_test.go
new file mode 100644
index 0000000000..152600bf29
--- /dev/null
+++ b/tests/integration/repo_watch_test.go
@@ -0,0 +1,25 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+func TestRepoWatch(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ // Test round-trip auto-watch
+ setting.Service.AutoWatchOnChanges = true
+ session := loginUser(t, "user2")
+ unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 2, RepoID: 3})
+ testEditFile(t, session, "user3", "repo3", "master", "README.md", "Hello, World (Edited for watch)\n")
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 2, RepoID: 3, Mode: repo_model.WatchModeAuto})
+ })
+}
diff --git a/tests/integration/repofiles_delete_test.go b/tests/integration/repofiles_delete_test.go
new file mode 100644
index 0000000000..f594efdeeb
--- /dev/null
+++ b/tests/integration/repofiles_delete_test.go
@@ -0,0 +1,202 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/url"
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
+ files_service "code.gitea.io/gitea/services/repository/files"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getDeleteRepoFileOptions(repo *repo_model.Repository) *files_service.DeleteRepoFileOptions {
+ return &files_service.DeleteRepoFileOptions{
+ LastCommitID: "",
+ OldBranch: repo.DefaultBranch,
+ NewBranch: repo.DefaultBranch,
+ TreePath: "README.md",
+ Message: "Deletes README.md",
+ SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+ Author: &files_service.IdentityOptions{
+ Name: "Bob Smith",
+ Email: "bob@smith.com",
+ },
+ Committer: nil,
+ }
+}
+
+func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
+ // Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined
+ return &api.FileResponse{
+ Content: nil,
+ Commit: &api.FileCommitResponse{
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Bob Smith",
+ Email: "bob@smith.com",
+ },
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Bob Smith",
+ Email: "bob@smith.com",
+ },
+ },
+ Message: "Deletes README.md\n",
+ },
+ Verification: &api.PayloadCommitVerification{
+ Verified: false,
+ Reason: "gpg.error.not_signed_commit",
+ Signature: "",
+ Payload: "",
+ },
+ }
+}
+
+func TestDeleteRepoFile(t *testing.T) {
+ onGiteaRun(t, testDeleteRepoFile)
+}
+
+func testDeleteRepoFile(t *testing.T, u *url.URL) {
+ // setup
+ unittest.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getDeleteRepoFileOptions(repo)
+
+ t.Run("Delete README.md file", func(t *testing.T) {
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.NoError(t, err)
+ expectedFileResponse := getExpectedDeleteFileResponse(u)
+ assert.NotNil(t, fileResponse)
+ assert.Nil(t, fileResponse.Content)
+ assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
+ assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
+ })
+
+ t.Run("Verify README.md has been deleted", func(t *testing.T) {
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ expectedError := "repository file does not exist [path: " + opts.TreePath + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+}
+
+// Test opts with branch names removed, same results
+func TestDeleteRepoFileWithoutBranchNames(t *testing.T) {
+ onGiteaRun(t, testDeleteRepoFileWithoutBranchNames)
+}
+
+func testDeleteRepoFileWithoutBranchNames(t *testing.T, u *url.URL) {
+ // setup
+ unittest.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getDeleteRepoFileOptions(repo)
+ opts.OldBranch = ""
+ opts.NewBranch = ""
+
+ t.Run("Delete README.md without Branch Name", func(t *testing.T) {
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.NoError(t, err)
+ expectedFileResponse := getExpectedDeleteFileResponse(u)
+ assert.NotNil(t, fileResponse)
+ assert.Nil(t, fileResponse.Content)
+ assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
+ assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
+ })
+}
+
+func TestDeleteRepoFileErrors(t *testing.T) {
+ // setup
+ unittest.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+
+ t.Run("Bad branch", func(t *testing.T) {
+ opts := getDeleteRepoFileOptions(repo)
+ opts.OldBranch = "bad_branch"
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Error(t, err)
+ assert.Nil(t, fileResponse)
+ expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("Bad SHA", func(t *testing.T) {
+ opts := getDeleteRepoFileOptions(repo)
+ origSHA := opts.SHA
+ opts.SHA = "bad_sha"
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("New branch already exists", func(t *testing.T) {
+ opts := getDeleteRepoFileOptions(repo)
+ opts.NewBranch = "develop"
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "branch already exists [name: " + opts.NewBranch + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("TreePath is empty:", func(t *testing.T) {
+ opts := getDeleteRepoFileOptions(repo)
+ opts.TreePath = ""
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "path contains a malformed path component [path: ]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("TreePath is a git directory:", func(t *testing.T) {
+ opts := getDeleteRepoFileOptions(repo)
+ opts.TreePath = ".git"
+ fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+}
diff --git a/tests/integration/repofiles_update_test.go b/tests/integration/repofiles_update_test.go
new file mode 100644
index 0000000000..c62c49eeeb
--- /dev/null
+++ b/tests/integration/repofiles_update_test.go
@@ -0,0 +1,416 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/url"
+ "path/filepath"
+ "testing"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
+ files_service "code.gitea.io/gitea/services/repository/files"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getCreateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
+ return &files_service.UpdateRepoFileOptions{
+ OldBranch: repo.DefaultBranch,
+ NewBranch: repo.DefaultBranch,
+ TreePath: "new/file.txt",
+ Message: "Creates new/file.txt",
+ Content: "This is a NEW file",
+ IsNewFile: true,
+ Author: nil,
+ Committer: nil,
+ }
+}
+
+func getUpdateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
+ return &files_service.UpdateRepoFileOptions{
+ OldBranch: repo.DefaultBranch,
+ NewBranch: repo.DefaultBranch,
+ TreePath: "README.md",
+ Message: "Updates README.md",
+ SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+ Content: "This is UPDATED content for the README file",
+ IsNewFile: false,
+ Author: nil,
+ Committer: nil,
+ }
+}
+
+func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) *api.FileResponse {
+ treePath := "new/file.txt"
+ encoding := "base64"
+ content := "VGhpcyBpcyBhIE5FVyBmaWxl"
+ selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
+ htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
+ gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885"
+ downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
+ return &api.FileResponse{
+ Content: &api.ContentsResponse{
+ Name: filepath.Base(treePath),
+ Path: treePath,
+ SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
+ LastCommitSHA: lastCommitSHA,
+ Type: "file",
+ Size: 18,
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
+ Links: &api.FileLinksResponse{
+ Self: &selfURL,
+ GitURL: &gitURL,
+ HTMLURL: &htmlURL,
+ },
+ },
+ Commit: &api.FileCommitResponse{
+ CommitMeta: api.CommitMeta{
+ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
+ SHA: commitID,
+ },
+ HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "User Two",
+ Email: "user2@noreply.example.org",
+ },
+ Date: time.Now().UTC().Format(time.RFC3339),
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "User Two",
+ Email: "user2@noreply.example.org",
+ },
+ Date: time.Now().UTC().Format(time.RFC3339),
+ },
+ Parents: []*api.CommitMeta{
+ {
+ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ },
+ },
+ Message: "Updates README.md\n",
+ Tree: &api.CommitMeta{
+ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
+ SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc",
+ },
+ },
+ Verification: &api.PayloadCommitVerification{
+ Verified: false,
+ Reason: "gpg.error.not_signed_commit",
+ Signature: "",
+ Payload: "",
+ },
+ }
+}
+
+func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA string) *api.FileResponse {
+ encoding := "base64"
+ content := "VGhpcyBpcyBVUERBVEVEIGNvbnRlbnQgZm9yIHRoZSBSRUFETUUgZmlsZQ=="
+ selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + filename + "?ref=master"
+ htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + filename
+ gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647"
+ downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + filename
+ return &api.FileResponse{
+ Content: &api.ContentsResponse{
+ Name: filename,
+ Path: filename,
+ SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647",
+ LastCommitSHA: lastCommitSHA,
+ Type: "file",
+ Size: 43,
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
+ Links: &api.FileLinksResponse{
+ Self: &selfURL,
+ GitURL: &gitURL,
+ HTMLURL: &htmlURL,
+ },
+ },
+ Commit: &api.FileCommitResponse{
+ CommitMeta: api.CommitMeta{
+ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
+ SHA: commitID,
+ },
+ HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "User Two",
+ Email: "user2@noreply.example.org",
+ },
+ Date: time.Now().UTC().Format(time.RFC3339),
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "User Two",
+ Email: "user2@noreply.example.org",
+ },
+ Date: time.Now().UTC().Format(time.RFC3339),
+ },
+ Parents: []*api.CommitMeta{
+ {
+ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ },
+ },
+ Message: "Updates README.md\n",
+ Tree: &api.CommitMeta{
+ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
+ SHA: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
+ },
+ },
+ Verification: &api.PayloadCommitVerification{
+ Verified: false,
+ Reason: "gpg.error.not_signed_commit",
+ Signature: "",
+ Payload: "",
+ },
+ }
+}
+
+func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
+ // setup
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getCreateRepoFileOptions(repo)
+
+ // test
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+
+ // asserts
+ assert.NoError(t, err)
+ gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ defer gitRepo.Close()
+
+ commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch)
+ lastCommit, _ := gitRepo.GetCommitByPath("new/file.txt")
+ expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String())
+ assert.NotNil(t, expectedFileResponse)
+ if expectedFileResponse != nil {
+ assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+ }
+ })
+}
+
+func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
+ // setup
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getUpdateRepoFileOptions(repo)
+
+ // test
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+
+ // asserts
+ assert.NoError(t, err)
+ gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ defer gitRepo.Close()
+
+ commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
+ lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
+ expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
+ assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+ })
+}
+
+func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
+ // setup
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getUpdateRepoFileOptions(repo)
+ opts.FromTreePath = "README.md"
+ opts.TreePath = "README_new.md" // new file name, README_new.md
+
+ // test
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+
+ // asserts
+ assert.NoError(t, err)
+ gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ defer gitRepo.Close()
+
+ commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
+ lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
+ expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
+ // assert that the old file no longer exists in the last commit of the branch
+ fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath)
+ switch err.(type) {
+ case git.ErrNotExist:
+ // correct, continue
+ default:
+ t.Fatalf("expected git.ErrNotExist, got:%v", err)
+ }
+ toEntry, err := commit.GetTreeEntryByPath(opts.TreePath)
+ assert.NoError(t, err)
+ assert.Nil(t, fromEntry) // Should no longer exist here
+ assert.NotNil(t, toEntry) // Should exist here
+ // assert SHA has remained the same but paths use the new file name
+ assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA)
+ assert.EqualValues(t, expectedFileResponse.Content.Name, fileResponse.Content.Name)
+ assert.EqualValues(t, expectedFileResponse.Content.Path, fileResponse.Content.Path)
+ assert.EqualValues(t, expectedFileResponse.Content.URL, fileResponse.Content.URL)
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
+ })
+}
+
+// Test opts with branch names removed, should get same results as above test
+func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
+ // setup
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getUpdateRepoFileOptions(repo)
+ opts.OldBranch = ""
+ opts.NewBranch = ""
+
+ // test
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+
+ // asserts
+ assert.NoError(t, err)
+ gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath())
+ defer gitRepo.Close()
+
+ commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch)
+ lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
+ expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
+ assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
+ })
+}
+
+func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
+ // setup
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+
+ t.Run("bad branch", func(t *testing.T) {
+ opts := getUpdateRepoFileOptions(repo)
+ opts.OldBranch = "bad_branch"
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Error(t, err)
+ assert.Nil(t, fileResponse)
+ expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("bad SHA", func(t *testing.T) {
+ opts := getUpdateRepoFileOptions(repo)
+ origSHA := opts.SHA
+ opts.SHA = "bad_sha"
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("new branch already exists", func(t *testing.T) {
+ opts := getUpdateRepoFileOptions(repo)
+ opts.NewBranch = "develop"
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "branch already exists [name: " + opts.NewBranch + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("treePath is empty:", func(t *testing.T) {
+ opts := getUpdateRepoFileOptions(repo)
+ opts.TreePath = ""
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "path contains a malformed path component [path: ]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("treePath is a git directory:", func(t *testing.T) {
+ opts := getUpdateRepoFileOptions(repo)
+ opts.TreePath = ".git"
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+
+ t.Run("create file that already exists", func(t *testing.T) {
+ opts := getCreateRepoFileOptions(repo)
+ opts.TreePath = "README.md" // already exists
+ fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, fileResponse)
+ assert.Error(t, err)
+ expectedError := "repository file already exists [path: " + opts.TreePath + "]"
+ assert.EqualError(t, err, expectedError)
+ })
+ })
+}
diff --git a/tests/integration/setting_test.go b/tests/integration/setting_test.go
new file mode 100644
index 0000000000..6273545c23
--- /dev/null
+++ b/tests/integration/setting_test.go
@@ -0,0 +1,108 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSettingShowUserEmailExplore(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ showUserEmail := setting.UI.ShowUserEmail
+ setting.UI.ShowUserEmail = true
+
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/explore/users")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Contains(t,
+ htmlDoc.doc.Find(".ui.user.list").Text(),
+ "user4@example.com",
+ )
+
+ setting.UI.ShowUserEmail = false
+
+ req = NewRequest(t, "GET", "/explore/users")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ assert.NotContains(t,
+ htmlDoc.doc.Find(".ui.user.list").Text(),
+ "user4@example.com",
+ )
+
+ setting.UI.ShowUserEmail = showUserEmail
+}
+
+func TestSettingShowUserEmailProfile(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ showUserEmail := setting.UI.ShowUserEmail
+ setting.UI.ShowUserEmail = true
+
+ session := loginUser(t, "user2")
+ req := NewRequest(t, "GET", "/user2")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Contains(t,
+ htmlDoc.doc.Find(".user.profile").Text(),
+ "user2@example.com",
+ )
+
+ setting.UI.ShowUserEmail = false
+
+ req = NewRequest(t, "GET", "/user2")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ // Should contain since this user owns the profile page
+ assert.Contains(t,
+ htmlDoc.doc.Find(".user.profile").Text(),
+ "user2@example.com",
+ )
+
+ setting.UI.ShowUserEmail = showUserEmail
+
+ session = loginUser(t, "user4")
+ req = NewRequest(t, "GET", "/user2")
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ assert.NotContains(t,
+ htmlDoc.doc.Find(".user.profile").Text(),
+ "user2@example.com",
+ )
+}
+
+func TestSettingLandingPage(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ landingPage := setting.LandingPageURL
+
+ setting.LandingPageURL = setting.LandingPageHome
+ req := NewRequest(t, "GET", "/")
+ MakeRequest(t, req, http.StatusOK)
+
+ setting.LandingPageURL = setting.LandingPageExplore
+ req = NewRequest(t, "GET", "/")
+ resp := MakeRequest(t, req, http.StatusSeeOther)
+ assert.Equal(t, "/explore", resp.Header().Get("Location"))
+
+ setting.LandingPageURL = setting.LandingPageOrganizations
+ req = NewRequest(t, "GET", "/")
+ resp = MakeRequest(t, req, http.StatusSeeOther)
+ assert.Equal(t, "/explore/organizations", resp.Header().Get("Location"))
+
+ setting.LandingPageURL = setting.LandingPageLogin
+ req = NewRequest(t, "GET", "/")
+ resp = MakeRequest(t, req, http.StatusSeeOther)
+ assert.Equal(t, "/user/login", resp.Header().Get("Location"))
+
+ setting.LandingPageURL = landingPage
+}
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
new file mode 100644
index 0000000000..7dc078e274
--- /dev/null
+++ b/tests/integration/signin_test.go
@@ -0,0 +1,60 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func testLoginFailed(t *testing.T, username, password, message string) {
+ session := emptyTestSession(t)
+ req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/login"),
+ "user_name": username,
+ "password": password,
+ })
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
+
+ assert.EqualValues(t, message, resultMsg)
+}
+
+func TestSignin(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ // add new user with user2's email
+ user.Name = "testuser"
+ user.LowerName = strings.ToLower(user.Name)
+ user.ID = 0
+ unittest.AssertSuccessfulInsert(t, user)
+
+ samples := []struct {
+ username string
+ password string
+ message string
+ }{
+ {username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
+ {username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
+ {username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
+ {username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")},
+ }
+
+ for _, s := range samples {
+ testLoginFailed(t, s.username, s.password, s.message)
+ }
+}
diff --git a/tests/integration/signout_test.go b/tests/integration/signout_test.go
new file mode 100644
index 0000000000..1f1346a5c3
--- /dev/null
+++ b/tests/integration/signout_test.go
@@ -0,0 +1,28 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+)
+
+func TestSignOut(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ req := NewRequest(t, "POST", "/user/logout")
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // try to view a private repo, should fail
+ req = NewRequest(t, "GET", "/user2/repo2")
+ session.MakeRequest(t, req, http.StatusNotFound)
+
+ // invalidate cached cookies for user2, for subsequent tests
+ delete(loginSessionCache, "user2")
+}
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go
new file mode 100644
index 0000000000..1c598fd0d1
--- /dev/null
+++ b/tests/integration/signup_test.go
@@ -0,0 +1,94 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSignup(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.Service.EnableCaptcha = false
+
+ req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
+ "user_name": "exampleUser",
+ "email": "exampleUser@example.com",
+ "password": "examplePassword!1",
+ "retype": "examplePassword!1",
+ })
+ MakeRequest(t, req, http.StatusSeeOther)
+
+ // should be able to view new user's page
+ req = NewRequest(t, "GET", "/exampleUser")
+ MakeRequest(t, req, http.StatusOK)
+}
+
+func TestSignupAsRestricted(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.Service.EnableCaptcha = false
+ setting.Service.DefaultUserIsRestricted = true
+
+ req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
+ "user_name": "restrictedUser",
+ "email": "restrictedUser@example.com",
+ "password": "examplePassword!1",
+ "retype": "examplePassword!1",
+ })
+ MakeRequest(t, req, http.StatusSeeOther)
+
+ // should be able to view new user's page
+ req = NewRequest(t, "GET", "/restrictedUser")
+ MakeRequest(t, req, http.StatusOK)
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "restrictedUser"})
+ assert.True(t, user2.IsRestricted)
+}
+
+func TestSignupEmail(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.Service.EnableCaptcha = false
+
+ tests := []struct {
+ email string
+ wantStatus int
+ wantMsg string
+ }{
+ {"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
+ {"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
+ {"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")},
+ {"exampleUser@example.com", http.StatusSeeOther, ""},
+ }
+
+ for i, test := range tests {
+ req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
+ "user_name": fmt.Sprintf("exampleUser%d", i),
+ "email": test.email,
+ "password": "examplePassword!1",
+ "retype": "examplePassword!1",
+ })
+ resp := MakeRequest(t, req, test.wantStatus)
+ if test.wantMsg != "" {
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Equal(t,
+ test.wantMsg,
+ strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
+ )
+ }
+ }
+}
diff --git a/tests/integration/ssh_key_test.go b/tests/integration/ssh_key_test.go
new file mode 100644
index 0000000000..65d9b84404
--- /dev/null
+++ b/tests/integration/ssh_key_test.go
@@ -0,0 +1,214 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func doCheckRepositoryEmptyStatus(ctx APITestContext, isEmpty bool) func(*testing.T) {
+ return doAPIGetRepository(ctx, func(t *testing.T, repository api.Repository) {
+ assert.Equal(t, isEmpty, repository.Empty)
+ })
+}
+
+func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) {
+ return func(t *testing.T) {
+ assert.NoError(t, os.WriteFile(filepath.Join(dstPath, filename), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s at time: %v", dstPath, time.Now())), 0o644))
+ assert.NoError(t, git.AddChanges(dstPath, true))
+ signature := git.Signature{
+ Email: "test@example.com",
+ Name: "test",
+ When: time.Now(),
+ }
+ assert.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{
+ Committer: &signature,
+ Author: &signature,
+ Message: "Initial Commit",
+ }))
+ }
+}
+
+func TestPushDeployKeyOnEmptyRepo(t *testing.T) {
+ onGiteaRun(t, testPushDeployKeyOnEmptyRepo)
+}
+
+func testPushDeployKeyOnEmptyRepo(t *testing.T, u *url.URL) {
+ // OK login
+ ctx := NewAPITestContext(t, "user2", "deploy-key-empty-repo-1")
+ keyname := fmt.Sprintf("%s-push", ctx.Reponame)
+ u.Path = ctx.GitPath()
+
+ t.Run("CreateEmptyRepository", doAPICreateRepository(ctx, true))
+
+ t.Run("CheckIsEmpty", doCheckRepositoryEmptyStatus(ctx, true))
+
+ withKeyFile(t, keyname, func(keyFile string) {
+ t.Run("CreatePushDeployKey", doAPICreateDeployKey(ctx, keyname, keyFile, false))
+
+ // Setup the testing repository
+ dstPath, err := os.MkdirTemp("", "repo-tmp-deploy-key-empty-repo-1")
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ t.Run("InitTestRepository", doGitInitTestRepository(dstPath))
+
+ // Setup remote link
+ sshURL := createSSHUrl(ctx.GitPath(), u)
+
+ t.Run("AddRemote", doGitAddRemote(dstPath, "origin", sshURL))
+
+ t.Run("SSHPushTestRepository", doGitPushTestRepository(dstPath, "origin", "master"))
+
+ t.Run("CheckIsNotEmpty", doCheckRepositoryEmptyStatus(ctx, false))
+
+ t.Run("DeleteRepository", doAPIDeleteRepository(ctx))
+ })
+}
+
+func TestKeyOnlyOneType(t *testing.T) {
+ onGiteaRun(t, testKeyOnlyOneType)
+}
+
+func testKeyOnlyOneType(t *testing.T, u *url.URL) {
+ // Once a key is a user key we cannot use it as a deploy key
+ // If we delete it from the user we should be able to use it as a deploy key
+ reponame := "ssh-key-test-repo"
+ username := "user2"
+ u.Path = fmt.Sprintf("%s/%s.git", username, reponame)
+ keyname := fmt.Sprintf("%s-push", reponame)
+
+ // OK login
+ ctx := NewAPITestContext(t, username, reponame)
+
+ otherCtx := ctx
+ otherCtx.Reponame = "ssh-key-test-repo-2"
+
+ failCtx := ctx
+ failCtx.ExpectedCode = http.StatusUnprocessableEntity
+
+ t.Run("CreateRepository", doAPICreateRepository(ctx, false))
+ t.Run("CreateOtherRepository", doAPICreateRepository(otherCtx, false))
+
+ withKeyFile(t, keyname, func(keyFile string) {
+ var userKeyPublicKeyID int64
+ t.Run("KeyCanOnlyBeUser", func(t *testing.T) {
+ dstPath, err := os.MkdirTemp("", ctx.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ sshURL := createSSHUrl(ctx.GitPath(), u)
+
+ t.Run("FailToClone", doGitCloneFail(sshURL))
+
+ t.Run("CreateUserKey", doAPICreateUserKey(ctx, keyname, keyFile, func(t *testing.T, publicKey api.PublicKey) {
+ userKeyPublicKeyID = publicKey.ID
+ }))
+
+ t.Run("FailToAddReadOnlyDeployKey", doAPICreateDeployKey(failCtx, keyname, keyFile, true))
+
+ t.Run("FailToAddDeployKey", doAPICreateDeployKey(failCtx, keyname, keyFile, false))
+
+ t.Run("Clone", doGitClone(dstPath, sshURL))
+
+ t.Run("AddChanges", doAddChangesToCheckout(dstPath, "CHANGES1.md"))
+
+ t.Run("Push", doGitPushTestRepository(dstPath, "origin", "master"))
+
+ t.Run("DeleteUserKey", doAPIDeleteUserKey(ctx, userKeyPublicKeyID))
+ })
+
+ t.Run("KeyCanBeAnyDeployButNotUserAswell", func(t *testing.T) {
+ dstPath, err := os.MkdirTemp("", ctx.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ sshURL := createSSHUrl(ctx.GitPath(), u)
+
+ t.Run("FailToClone", doGitCloneFail(sshURL))
+
+ // Should now be able to add...
+ t.Run("AddReadOnlyDeployKey", doAPICreateDeployKey(ctx, keyname, keyFile, true))
+
+ t.Run("Clone", doGitClone(dstPath, sshURL))
+
+ t.Run("AddChanges", doAddChangesToCheckout(dstPath, "CHANGES2.md"))
+
+ t.Run("FailToPush", doGitPushTestRepositoryFail(dstPath, "origin", "master"))
+
+ otherSSHURL := createSSHUrl(otherCtx.GitPath(), u)
+ dstOtherPath, err := os.MkdirTemp("", otherCtx.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstOtherPath)
+
+ t.Run("AddWriterDeployKeyToOther", doAPICreateDeployKey(otherCtx, keyname, keyFile, false))
+
+ t.Run("CloneOther", doGitClone(dstOtherPath, otherSSHURL))
+
+ t.Run("AddChangesToOther", doAddChangesToCheckout(dstOtherPath, "CHANGES3.md"))
+
+ t.Run("PushToOther", doGitPushTestRepository(dstOtherPath, "origin", "master"))
+
+ t.Run("FailToCreateUserKey", doAPICreateUserKey(failCtx, keyname, keyFile))
+ })
+
+ t.Run("DeleteRepositoryShouldReleaseKey", func(t *testing.T) {
+ otherSSHURL := createSSHUrl(otherCtx.GitPath(), u)
+ dstOtherPath, err := os.MkdirTemp("", otherCtx.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstOtherPath)
+
+ t.Run("DeleteRepository", doAPIDeleteRepository(ctx))
+
+ t.Run("FailToCreateUserKeyAsStillDeploy", doAPICreateUserKey(failCtx, keyname, keyFile))
+
+ t.Run("MakeSureCloneOtherStillWorks", doGitClone(dstOtherPath, otherSSHURL))
+
+ t.Run("AddChangesToOther", doAddChangesToCheckout(dstOtherPath, "CHANGES3.md"))
+
+ t.Run("PushToOther", doGitPushTestRepository(dstOtherPath, "origin", "master"))
+
+ t.Run("DeleteOtherRepository", doAPIDeleteRepository(otherCtx))
+
+ t.Run("RecreateRepository", doAPICreateRepository(ctx, false))
+
+ t.Run("CreateUserKey", doAPICreateUserKey(ctx, keyname, keyFile, func(t *testing.T, publicKey api.PublicKey) {
+ userKeyPublicKeyID = publicKey.ID
+ }))
+
+ dstPath, err := os.MkdirTemp("", ctx.Reponame)
+ assert.NoError(t, err)
+ defer util.RemoveAll(dstPath)
+
+ sshURL := createSSHUrl(ctx.GitPath(), u)
+
+ t.Run("Clone", doGitClone(dstPath, sshURL))
+
+ t.Run("AddChanges", doAddChangesToCheckout(dstPath, "CHANGES1.md"))
+
+ t.Run("Push", doGitPushTestRepository(dstPath, "origin", "master"))
+ })
+
+ t.Run("DeleteUserKeyShouldRemoveAbilityToClone", func(t *testing.T) {
+ sshURL := createSSHUrl(ctx.GitPath(), u)
+
+ t.Run("DeleteUserKey", doAPIDeleteUserKey(ctx, userKeyPublicKeyID))
+
+ t.Run("FailToClone", doGitCloneFail(sshURL))
+ })
+ })
+}
diff --git a/tests/integration/timetracking_test.go b/tests/integration/timetracking_test.go
new file mode 100644
index 0000000000..54b81ff3bc
--- /dev/null
+++ b/tests/integration/timetracking_test.go
@@ -0,0 +1,82 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "path"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViewTimetrackingControls(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ testViewTimetrackingControls(t, session, "user2", "repo1", "1", true)
+ // user2/repo1
+}
+
+func TestNotViewTimetrackingControls(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user5")
+ testViewTimetrackingControls(t, session, "user2", "repo1", "1", false)
+ // user2/repo1
+}
+
+func TestViewTimetrackingControlsDisabled(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user2")
+ testViewTimetrackingControls(t, session, "user3", "repo3", "1", false)
+}
+
+func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo, issue string, canTrackTime bool) {
+ req := NewRequest(t, "GET", path.Join(user, repo, "issues", issue))
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ htmlDoc.AssertElement(t, ".timetrack .issue-start-time", canTrackTime)
+ htmlDoc.AssertElement(t, ".timetrack .issue-add-time", canTrackTime)
+
+ req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ })
+ if canTrackTime {
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequest(t, "GET", test.RedirectURL(resp))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+
+ events := htmlDoc.doc.Find(".event > span.text")
+ assert.Contains(t, events.Last().Text(), "started working")
+
+ htmlDoc.AssertElement(t, ".timetrack .issue-stop-time", true)
+ htmlDoc.AssertElement(t, ".timetrack .issue-cancel-time", true)
+
+ // Sleep for 1 second to not get wrong order for stopping timer
+ time.Sleep(time.Second)
+
+ req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{
+ "_csrf": htmlDoc.GetCSRF(),
+ })
+ resp = session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequest(t, "GET", test.RedirectURL(resp))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc = NewHTMLParser(t, resp.Body)
+
+ events = htmlDoc.doc.Find(".event > span.text")
+ assert.Contains(t, events.Last().Text(), "stopped working")
+ htmlDoc.AssertElement(t, ".event .detail .octicon-clock", true)
+ } else {
+ session.MakeRequest(t, req, http.StatusNotFound)
+ }
+}
diff --git a/tests/integration/user_avatar_test.go b/tests/integration/user_avatar_test.go
new file mode 100644
index 0000000000..35be840c29
--- /dev/null
+++ b/tests/integration/user_avatar_test.go
@@ -0,0 +1,82 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "image/png"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/avatar"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUserAvatar(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo3, is an org
+
+ seed := user2.Email
+ if len(seed) == 0 {
+ seed = user2.Name
+ }
+
+ img, err := avatar.RandomImage([]byte(seed))
+ if err != nil {
+ assert.NoError(t, err)
+ return
+ }
+
+ session := loginUser(t, "user2")
+ csrf := GetCSRF(t, session, "/user/settings")
+
+ imgData := &bytes.Buffer{}
+
+ body := &bytes.Buffer{}
+
+ // Setup multi-part
+ writer := multipart.NewWriter(body)
+ writer.WriteField("source", "local")
+ part, err := writer.CreateFormFile("avatar", "avatar-for-testuseravatar.png")
+ if err != nil {
+ assert.NoError(t, err)
+ return
+ }
+
+ if err := png.Encode(imgData, img); err != nil {
+ assert.NoError(t, err)
+ return
+ }
+
+ if _, err := io.Copy(part, imgData); err != nil {
+ assert.NoError(t, err)
+ return
+ }
+
+ if err := writer.Close(); err != nil {
+ assert.NoError(t, err)
+ return
+ }
+
+ req := NewRequestWithBody(t, "POST", "/user/settings/avatar", body)
+ req.Header.Add("X-Csrf-Token", csrf)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo3, is an org
+
+ req = NewRequest(t, "GET", user2.AvatarLinkWithSize(0))
+ _ = session.MakeRequest(t, req, http.StatusOK)
+
+ // Can't test if the response matches because the image is re-generated on upload but checking that this at least doesn't give a 404 should be enough.
+ })
+}
diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go
new file mode 100644
index 0000000000..110f5c89bf
--- /dev/null
+++ b/tests/integration/user_test.go
@@ -0,0 +1,251 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/modules/translation"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViewUser(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ req := NewRequest(t, "GET", "/user2")
+ MakeRequest(t, req, http.StatusOK)
+}
+
+func TestRenameUsername(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": "newUsername",
+ "email": "user2@example.com",
+ "language": "en-US",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "newUsername"})
+ unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
+}
+
+func TestRenameInvalidUsername(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ invalidUsernames := []string{
+ "%2f*",
+ "%2f.",
+ "%2f..",
+ "%00",
+ "thisHas ASpace",
+ "p<A>tho>lo<gical",
+ }
+
+ session := loginUser(t, "user2")
+ for _, invalidUsername := range invalidUsernames {
+ t.Logf("Testing username %s", invalidUsername)
+
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": invalidUsername,
+ "email": "user2@example.com",
+ })
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Contains(t,
+ htmlDoc.doc.Find(".ui.negative.message").Text(),
+ translation.NewLocale("en-US").Tr("form.alpha_dash_dot_error"),
+ )
+
+ unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
+ }
+}
+
+func TestRenameReservedUsername(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ reservedUsernames := []string{
+ ".",
+ "..",
+ ".well-known",
+ "admin",
+ "api",
+ "assets",
+ "attachments",
+ "avatar",
+ "avatars",
+ "captcha",
+ "commits",
+ "debug",
+ "error",
+ "explore",
+ "favicon.ico",
+ "ghost",
+ "issues",
+ "login",
+ "manifest.json",
+ "metrics",
+ "milestones",
+ "new",
+ "notifications",
+ "org",
+ "pulls",
+ "raw",
+ "repo",
+ "repo-avatars",
+ "robots.txt",
+ "search",
+ "serviceworker.js",
+ "ssh_info",
+ "swagger.v1.json",
+ "user",
+ "v2",
+ }
+
+ session := loginUser(t, "user2")
+ for _, reservedUsername := range reservedUsernames {
+ t.Logf("Testing username %s", reservedUsername)
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": reservedUsername,
+ "email": "user2@example.com",
+ "language": "en-US",
+ })
+ resp := session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequest(t, "GET", test.RedirectURL(resp))
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.Contains(t,
+ htmlDoc.doc.Find(".ui.negative.message").Text(),
+ translation.NewLocale("en-US").Tr("user.form.name_reserved", reservedUsername),
+ )
+
+ unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername})
+ }
+}
+
+func TestExportUserGPGKeys(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ // Export empty key list
+ testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK-----
+Note: This user hasn't uploaded any GPG keys.
+
+
+=twTO
+-----END PGP PUBLIC KEY BLOCK-----
+`)
+ // Import key
+ // User1 <user1@example.com>
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+ testCreateGPGKey(t, session.MakeRequest, token, http.StatusCreated, `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFyy/VUBCADJ7zbM20Z1RWmFoVgp5WkQfI2rU1Vj9cQHes9i42wVLLtcbPeo
+QzubgzvMPITDy7nfWxgSf83E23DoHQ1ACFbQh/6eFSRrjsusp3YQ/08NSfPPbcu8
+0M5G+VGwSfzS5uEcwBVQmHyKdcOZIERTNMtYZx1C3bjLD1XVJHvWz9D72Uq4qeO3
+8SR+lzp5n6ppUakcmRnxt3nGRBj1+hEGkdgzyPo93iy+WioegY2lwCA9xMEo5dah
+BmYxWx51zyiXYlReTaxlyb3/nuSUt8IcW3Q8zjdtJj4Nu8U1SpV8EdaA1I9IPbHW
+510OSLmD3XhqHH5m6mIxL1YoWxk3V7gpDROtABEBAAG0GVVzZXIxIDx1c2VyMUBl
+eGFtcGxlLmNvbT6JAU4EEwEIADgWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9
+VQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9+v0I6RSEH22YCACFqL5+
+6M0m18AMC/pumcpnnmvAS1GrrKTF8nOROA1augZwp1WCNuKw2R6uOJIHANrYECSn
+u7+j6GBP2gbIW8mSAzS6HWCs7GGiPpVtT4wcu8wljUI6BxjpyZtoEkriyBjt6HfK
+rkegbkuySoJvjq4IcO5D1LB1JWgsUjMYQJj/ZpBIzVtjG9QtFSOiT1Hct4PoZHdC
+nsdSgyCkwRZXG+u3kT/wP9F663ba4o16vYlz3dCGo66lF2tyoG3qcyZ1OUzUrnuv
+96ytAzT6XIhrE0nVoBprMxFF5zExotJD3bHjcGBFNLf944bhjKee3U6t9+OsfJVC
+l7N5xxIawCuTQdbfuQENBFyy/VUBCADe61yGEoTwKfsOKIhxLaNoRmD883O0tiWt
+soO/HPj9dPQLTOiwXgSgSCd8C+LNxGKct87wgFozpah4tDLC6c0nALuHJ0SLbkfz
+55aRhLeOOcrAydatDp72GroXzqpZ0xZBk5wjIWdgEol2GmVRM8QGbeuakU/HVz5y
+lPzxUUocgdbSi3GE3zbzijQzVJdyL/kw/KP7pKT/PPKKJ2C5NQDLy0XGKEHddXGR
+EWKkVlRalxq/TjfaMR0bi3MpezBsQmp99ATPO/d7trayZUxQHRtXzGFiOXfDHATr
+qN730sODjqvU+mpc/SHCRwh9qWDjZRHSuKU5YDBjb5jIQJivZsQ/ABEBAAGJATYE
+GAEIACAWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9VQIbDAAKCRD9+v0I6RSE
+H7WoB/4tXl+97rQ6owPCGSVp1Xbwt2521V7COgsOFRVTRTryEWxRW8mm0S7wQvax
+C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6
+21dccpqchByVw/UMDeHSyjQLiG2lxzt8Gfx2gHmSbrq3aWovTGyz6JTffZvfy/n2
+0Hm437OBPazO0gZyXhdV2PE5RSUfvAgm44235tcV5EV0d32TJDfv61+Vr2GUbah6
+7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M
+GrE0MHOxUbc9tbtyk0F1SuzREUBH
+=DDXw
+-----END PGP PUBLIC KEY BLOCK-----
+`)
+ // Export new key
+ testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+xsBNBFyy/VUBCADJ7zbM20Z1RWmFoVgp5WkQfI2rU1Vj9cQHes9i42wVLLtcbPeo
+QzubgzvMPITDy7nfWxgSf83E23DoHQ1ACFbQh/6eFSRrjsusp3YQ/08NSfPPbcu8
+0M5G+VGwSfzS5uEcwBVQmHyKdcOZIERTNMtYZx1C3bjLD1XVJHvWz9D72Uq4qeO3
+8SR+lzp5n6ppUakcmRnxt3nGRBj1+hEGkdgzyPo93iy+WioegY2lwCA9xMEo5dah
+BmYxWx51zyiXYlReTaxlyb3/nuSUt8IcW3Q8zjdtJj4Nu8U1SpV8EdaA1I9IPbHW
+510OSLmD3XhqHH5m6mIxL1YoWxk3V7gpDROtABEBAAHNGVVzZXIxIDx1c2VyMUBl
+eGFtcGxlLmNvbT7CwI4EEwEIADgWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9
+VQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9+v0I6RSEH22YCACFqL5+
+6M0m18AMC/pumcpnnmvAS1GrrKTF8nOROA1augZwp1WCNuKw2R6uOJIHANrYECSn
+u7+j6GBP2gbIW8mSAzS6HWCs7GGiPpVtT4wcu8wljUI6BxjpyZtoEkriyBjt6HfK
+rkegbkuySoJvjq4IcO5D1LB1JWgsUjMYQJj/ZpBIzVtjG9QtFSOiT1Hct4PoZHdC
+nsdSgyCkwRZXG+u3kT/wP9F663ba4o16vYlz3dCGo66lF2tyoG3qcyZ1OUzUrnuv
+96ytAzT6XIhrE0nVoBprMxFF5zExotJD3bHjcGBFNLf944bhjKee3U6t9+OsfJVC
+l7N5xxIawCuTQdbfzsBNBFyy/VUBCADe61yGEoTwKfsOKIhxLaNoRmD883O0tiWt
+soO/HPj9dPQLTOiwXgSgSCd8C+LNxGKct87wgFozpah4tDLC6c0nALuHJ0SLbkfz
+55aRhLeOOcrAydatDp72GroXzqpZ0xZBk5wjIWdgEol2GmVRM8QGbeuakU/HVz5y
+lPzxUUocgdbSi3GE3zbzijQzVJdyL/kw/KP7pKT/PPKKJ2C5NQDLy0XGKEHddXGR
+EWKkVlRalxq/TjfaMR0bi3MpezBsQmp99ATPO/d7trayZUxQHRtXzGFiOXfDHATr
+qN730sODjqvU+mpc/SHCRwh9qWDjZRHSuKU5YDBjb5jIQJivZsQ/ABEBAAHCwHYE
+GAEIACAWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9VQIbDAAKCRD9+v0I6RSE
+H7WoB/4tXl+97rQ6owPCGSVp1Xbwt2521V7COgsOFRVTRTryEWxRW8mm0S7wQvax
+C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6
+21dccpqchByVw/UMDeHSyjQLiG2lxzt8Gfx2gHmSbrq3aWovTGyz6JTffZvfy/n2
+0Hm437OBPazO0gZyXhdV2PE5RSUfvAgm44235tcV5EV0d32TJDfv61+Vr2GUbah6
+7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M
+GrE0MHOxUbc9tbtyk0F1SuzREUBH
+=WFf5
+-----END PGP PUBLIC KEY BLOCK-----
+`)
+}
+
+func testExportUserGPGKeys(t *testing.T, user, expected string) {
+ session := loginUser(t, user)
+ t.Logf("Testing username %s export gpg keys", user)
+ req := NewRequest(t, "GET", "/"+user+".gpg")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ // t.Log(resp.Body.String())
+ assert.Equal(t, expected, resp.Body.String())
+}
+
+func TestListStopWatches(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, owner.Name)
+ req := NewRequestf(t, "GET", "/user/stopwatches")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ var apiWatches []*api.StopWatch
+ DecodeJSON(t, resp, &apiWatches)
+ stopwatch := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: owner.ID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: stopwatch.IssueID})
+ if assert.Len(t, apiWatches, 1) {
+ assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
+ assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
+ assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
+ assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
+ assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
+ assert.Greater(t, apiWatches[0].Seconds, int64(0))
+ }
+}
diff --git a/tests/integration/version_test.go b/tests/integration/version_test.go
new file mode 100644
index 0000000000..83be62d3f4
--- /dev/null
+++ b/tests/integration/version_test.go
@@ -0,0 +1,28 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestVersion(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.AppVer = "test-version-1"
+ req := NewRequest(t, "GET", "/api/v1/version")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var version structs.ServerVersion
+ DecodeJSON(t, resp, &version)
+ assert.Equal(t, setting.AppVer, version.Version)
+}
diff --git a/tests/integration/view_test.go b/tests/integration/view_test.go
new file mode 100644
index 0000000000..63544dbe35
--- /dev/null
+++ b/tests/integration/view_test.go
@@ -0,0 +1,27 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderFileSVGIsInImgTag(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user2")
+
+ req := NewRequest(t, "GET", "/user2/repo2/src/branch/master/line.svg")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ doc := NewHTMLParser(t, resp.Body)
+ src, exists := doc.doc.Find(".file-view img").Attr("src")
+ assert.True(t, exists, "The SVG image should be in an <img> tag so that scripts in the SVG are not run")
+ assert.Equal(t, "/user2/repo2/raw/branch/master/line.svg", src)
+}
diff --git a/tests/integration/webfinger_test.go b/tests/integration/webfinger_test.go
new file mode 100644
index 0000000000..bb3447c809
--- /dev/null
+++ b/tests/integration/webfinger_test.go
@@ -0,0 +1,69 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWebfinger(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ setting.Federation.Enabled = true
+ defer func() {
+ setting.Federation.Enabled = false
+ }()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ appURL, _ := url.Parse(setting.AppURL)
+
+ type webfingerLink struct {
+ Rel string `json:"rel,omitempty"`
+ Type string `json:"type,omitempty"`
+ Href string `json:"href,omitempty"`
+ Titles map[string]string `json:"titles,omitempty"`
+ Properties map[string]interface{} `json:"properties,omitempty"`
+ }
+
+ type webfingerJRD struct {
+ Subject string `json:"subject,omitempty"`
+ Aliases []string `json:"aliases,omitempty"`
+ Properties map[string]interface{} `json:"properties,omitempty"`
+ Links []*webfingerLink `json:"links,omitempty"`
+ }
+
+ session := loginUser(t, "user1")
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, appURL.Host))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var jrd webfingerJRD
+ DecodeJSON(t, resp, &jrd)
+ assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject)
+ assert.ElementsMatch(t, []string{user.HTMLURL(), appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(user.Name)}, jrd.Aliases)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, "unknown.host"))
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", "user31", appURL.Host))
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", "user31", appURL.Host))
+ session.MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=mailto:%s", user.Email))
+ MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/xss_test.go b/tests/integration/xss_test.go
new file mode 100644
index 0000000000..53b23072ad
--- /dev/null
+++ b/tests/integration/xss_test.go
@@ -0,0 +1,40 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestXSSUserFullName(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ const fullName = `name & <script class="evil">alert('Oh no!');</script>`
+
+ session := loginUser(t, user.Name)
+ req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+ "_csrf": GetCSRF(t, session, "/user/settings"),
+ "name": user.Name,
+ "full_name": fullName,
+ "email": user.Email,
+ "language": "en-US",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+
+ req = NewRequestf(t, "GET", "/%s", user.Name)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ assert.EqualValues(t, 0, htmlDoc.doc.Find("script.evil").Length())
+ assert.EqualValues(t, fullName,
+ htmlDoc.doc.Find("div.content").Find(".header.text.center").Text(),
+ )
+}
diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl
new file mode 100644
index 0000000000..9cec6169f9
--- /dev/null
+++ b/tests/mssql.ini.tmpl
@@ -0,0 +1,110 @@
+APP_NAME = Gitea: Git with a cup of tea
+RUN_MODE = prod
+
+[database]
+DB_TYPE = mssql
+HOST = {{TEST_MSSQL_HOST}}
+NAME = {{TEST_MSSQL_DBNAME}}
+USER = {{TEST_MSSQL_USERNAME}}
+PASSWD = {{TEST_MSSQL_PASSWORD}}
+SSL_MODE = disable
+
+[indexer]
+REPO_INDEXER_ENABLED = true
+REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/repos.bleve
+
+[queue.issue_indexer]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/issues.bleve
+DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/issues.queue
+
+[queue]
+TYPE = immediate
+
+[queue.code_indexer]
+TYPE = immediate
+
+[queue.push_update]
+TYPE = immediate
+
+[repository]
+ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/gitea-repositories
+
+[repository.local]
+LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/local-repo
+
+[repository.upload]
+TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/uploads
+
+[repository.signing]
+SIGNING_KEY = none
+
+[server]
+SSH_DOMAIN = localhost
+HTTP_PORT = 3003
+ROOT_URL = http://localhost:3003/
+DISABLE_SSH = false
+SSH_LISTEN_HOST = localhost
+SSH_PORT = 2201
+START_SSH_SERVER = true
+LFS_START_SERVER = true
+OFFLINE_MODE = false
+LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
+APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data
+BUILTIN_SSH_SERVER_USER = git
+SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
+
+[attachment]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/attachments
+
+[mailer]
+ENABLED = true
+MAILER_TYPE = dummy
+FROM = mssql-{{TEST_TYPE}}-test@gitea.io
+
+[service]
+REGISTER_EMAIL_CONFIRM = false
+REGISTER_MANUAL_CONFIRM = false
+ENABLE_NOTIFY_MAIL = false
+DISABLE_REGISTRATION = false
+ENABLE_CAPTCHA = false
+REQUIRE_SIGNIN_VIEW = false
+DEFAULT_KEEP_EMAIL_PRIVATE = false
+DEFAULT_ALLOW_CREATE_ORGANIZATION = true
+NO_REPLY_ADDRESS = noreply.example.org
+ENABLE_NOTIFY_MAIL = true
+
+[picture]
+DISABLE_GRAVATAR = false
+ENABLE_FEDERATED_AVATAR = false
+AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/repo-avatars
+
+[session]
+PROVIDER = file
+PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/sessions
+
+[log]
+MODE = {{TEST_LOGGER}}
+ROOT_PATH = {{REPO_TEST_DIR}}mssql-log
+ROUTER = ,
+XORM = file
+ENABLE_SSH_LOG = true
+
+[log.test]
+LEVEL = Info
+COLORIZE = true
+
+[log.file]
+LEVEL = Debug
+
+[security]
+DISABLE_GIT_HOOKS = false
+INSTALL_LOCK = true
+SECRET_KEY = 9pCviYTWSb
+INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
+
+[lfs]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/lfs
+
+[packages]
+ENABLED = true
diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl
new file mode 100644
index 0000000000..24a9a02dc4
--- /dev/null
+++ b/tests/mysql.ini.tmpl
@@ -0,0 +1,126 @@
+APP_NAME = Gitea: Git with a cup of tea
+RUN_MODE = prod
+
+[database]
+DB_TYPE = mysql
+HOST = {{TEST_MYSQL_HOST}}
+NAME = {{TEST_MYSQL_DBNAME}}
+USER = {{TEST_MYSQL_USERNAME}}
+PASSWD = {{TEST_MYSQL_PASSWORD}}
+SSL_MODE = disable
+
+[indexer]
+REPO_INDEXER_ENABLED = true
+REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/indexers/repos.bleve
+
+[queue.issue_indexer]
+TYPE = elasticsearch
+CONN_STR = http://elastic:changeme@elasticsearch:9200
+DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/indexers/issues.queue
+
+[queue]
+TYPE = immediate
+
+[queue.code_indexer]
+TYPE = immediate
+
+[queue.push_update]
+TYPE = immediate
+
+[repository]
+ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/gitea-repositories
+
+[repository.local]
+LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/tmp/local-repo
+
+[repository.upload]
+TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/tmp/uploads
+
+[repository.signing]
+SIGNING_KEY = none
+
+[server]
+SSH_DOMAIN = localhost
+HTTP_PORT = 3001
+ROOT_URL = http://localhost:3001/
+DISABLE_SSH = false
+SSH_LISTEN_HOST = localhost
+SSH_PORT = 2201
+APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data
+BUILTIN_SSH_SERVER_USER = git
+START_SSH_SERVER = true
+OFFLINE_MODE = false
+
+LFS_START_SERVER = true
+LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
+SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
+
+[lfs]
+MINIO_BASE_PATH = lfs/
+
+[attachment]
+MINIO_BASE_PATH = attachments/
+
+[avatars]
+MINIO_BASE_PATH = avatars/
+
+[repo-avatars]
+MINIO_BASE_PATH = repo-avatars/
+
+[storage]
+STORAGE_TYPE = minio
+SERVE_DIRECT = false
+MINIO_ENDPOINT = minio:9000
+MINIO_ACCESS_KEY_ID = 123456
+MINIO_SECRET_ACCESS_KEY = 12345678
+MINIO_BUCKET = gitea
+MINIO_LOCATION = us-east-1
+MINIO_USE_SSL = false
+
+[mailer]
+ENABLED = true
+MAILER_TYPE = dummy
+FROM = mysql-{{TEST_TYPE}}-test@gitea.io
+
+[service]
+REGISTER_EMAIL_CONFIRM = false
+REGISTER_MANUAL_CONFIRM = false
+ENABLE_NOTIFY_MAIL = false
+DISABLE_REGISTRATION = false
+ENABLE_CAPTCHA = false
+REQUIRE_SIGNIN_VIEW = false
+DEFAULT_KEEP_EMAIL_PRIVATE = false
+DEFAULT_ALLOW_CREATE_ORGANIZATION = true
+NO_REPLY_ADDRESS = noreply.example.org
+ENABLE_NOTIFY_MAIL = true
+
+[picture]
+DISABLE_GRAVATAR = false
+ENABLE_FEDERATED_AVATAR = false
+
+[session]
+PROVIDER = file
+PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/sessions
+
+[log]
+MODE = {{TEST_LOGGER}}
+ROOT_PATH = {{REPO_TEST_DIR}}mysql-log
+ROUTER = ,
+XORM = file
+ENABLE_SSH_LOG = true
+
+[log.test]
+LEVEL = Info
+COLORIZE = true
+
+[log.file]
+LEVEL = Debug
+
+[security]
+DISABLE_GIT_HOOKS = false
+INSTALL_LOCK = true
+SECRET_KEY = 9pCviYTWSb
+INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
+
+[packages]
+ENABLED = true
diff --git a/tests/mysql8.ini.tmpl b/tests/mysql8.ini.tmpl
new file mode 100644
index 0000000000..f290efe1dc
--- /dev/null
+++ b/tests/mysql8.ini.tmpl
@@ -0,0 +1,107 @@
+APP_NAME = Gitea: Git with a cup of tea
+RUN_MODE = prod
+
+[database]
+DB_TYPE = mysql
+HOST = {{TEST_MYSQL8_HOST}}
+NAME = {{TEST_MYSQL8_DBNAME}}
+USER = {{TEST_MYSQL8_USERNAME}}
+PASSWD = {{TEST_MYSQL8_PASSWORD}}
+SSL_MODE = disable
+
+[indexer]
+REPO_INDEXER_ENABLED = true
+REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/indexers/repos.bleve
+
+[queue.issue_indexer]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/indexers/issues.bleve
+DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/indexers/issues.queue
+
+[queue]
+TYPE = immediate
+
+[queue.code_indexer]
+TYPE = immediate
+
+[queue.push_update]
+TYPE = immediate
+
+[repository]
+ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/gitea-repositories
+
+[repository.local]
+LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/tmp/local-repo
+
+[repository.upload]
+TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/tmp/uploads
+
+[repository.signing]
+SIGNING_KEY = none
+
+[server]
+SSH_DOMAIN = localhost
+HTTP_PORT = 3004
+ROOT_URL = http://localhost:3004/
+DISABLE_SSH = false
+SSH_LISTEN_HOST = localhost
+SSH_PORT = 2204
+START_SSH_SERVER = true
+LFS_START_SERVER = true
+OFFLINE_MODE = false
+LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
+APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/data
+BUILTIN_SSH_SERVER_USER = git
+SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
+
+[attachment]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/data/attachments
+
+[mailer]
+ENABLED = false
+
+[service]
+REGISTER_EMAIL_CONFIRM = false
+REGISTER_MANUAL_CONFIRM = false
+ENABLE_NOTIFY_MAIL = false
+DISABLE_REGISTRATION = false
+ENABLE_CAPTCHA = false
+REQUIRE_SIGNIN_VIEW = false
+DEFAULT_KEEP_EMAIL_PRIVATE = false
+DEFAULT_ALLOW_CREATE_ORGANIZATION = true
+NO_REPLY_ADDRESS = noreply.example.org
+
+[picture]
+DISABLE_GRAVATAR = false
+ENABLE_FEDERATED_AVATAR = false
+AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/data/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/data/repo-avatars
+
+[session]
+PROVIDER = file
+PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/data/sessions
+
+[log]
+MODE = {{TEST_LOGGER}}
+ROOT_PATH = {{REPO_TEST_DIR}}mysql8-log
+ROUTER = ,
+XORM = file
+ENABLE_SSH_LOG = true
+
+[log.test]
+LEVEL = Info
+COLORIZE = true
+
+[log.file]
+LEVEL = Debug
+
+[security]
+DISABLE_GIT_HOOKS = false
+INSTALL_LOCK = true
+SECRET_KEY = 9pCviYTWSb
+INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
+
+[lfs]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql8/data/lfs
+
+[packages]
+ENABLED = true
diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl
new file mode 100644
index 0000000000..c39b6a79c3
--- /dev/null
+++ b/tests/pgsql.ini.tmpl
@@ -0,0 +1,111 @@
+APP_NAME = Gitea: Git with a cup of tea
+RUN_MODE = prod
+
+[database]
+DB_TYPE = postgres
+HOST = {{TEST_PGSQL_HOST}}
+NAME = {{TEST_PGSQL_DBNAME}}
+USER = {{TEST_PGSQL_USERNAME}}
+PASSWD = {{TEST_PGSQL_PASSWORD}}
+SCHEMA = {{TEST_PGSQL_SCHEMA}}
+SSL_MODE = disable
+
+[indexer]
+REPO_INDEXER_ENABLED = true
+REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/repos.bleve
+
+[queue.issue_indexer]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/issues.bleve
+DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/issues.queue
+
+[queue]
+TYPE = immediate
+
+[queue.code_indexer]
+TYPE = immediate
+
+[queue.push_update]
+TYPE = immediate
+
+[repository]
+ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/gitea-repositories
+
+[repository.local]
+LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/tmp/local-repo
+
+[repository.upload]
+TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/tmp/uploads
+
+[repository.signing]
+SIGNING_KEY = none
+
+[server]
+SSH_DOMAIN = localhost
+HTTP_PORT = 3002
+ROOT_URL = http://localhost:3002/
+DISABLE_SSH = false
+SSH_LISTEN_HOST = localhost
+SSH_PORT = 2202
+START_SSH_SERVER = true
+LFS_START_SERVER = true
+OFFLINE_MODE = false
+LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
+APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data
+BUILTIN_SSH_SERVER_USER = git
+SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
+
+[attachment]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/attachments
+
+[mailer]
+ENABLED = true
+MAILER_TYPE = dummy
+FROM = pgsql-{{TEST_TYPE}}-test@gitea.io
+
+[service]
+REGISTER_EMAIL_CONFIRM = false
+REGISTER_MANUAL_CONFIRM = false
+ENABLE_NOTIFY_MAIL = false
+DISABLE_REGISTRATION = false
+ENABLE_CAPTCHA = false
+REQUIRE_SIGNIN_VIEW = false
+DEFAULT_KEEP_EMAIL_PRIVATE = false
+DEFAULT_ALLOW_CREATE_ORGANIZATION = true
+NO_REPLY_ADDRESS = noreply.example.org
+ENABLE_NOTIFY_MAIL = true
+
+[picture]
+DISABLE_GRAVATAR = false
+ENABLE_FEDERATED_AVATAR = false
+AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/repo-avatars
+
+[session]
+PROVIDER = file
+PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/sessions
+
+[log]
+MODE = {{TEST_LOGGER}}
+ROOT_PATH = {{REPO_TEST_DIR}}pgsql-log
+ROUTER = ,
+XORM = file
+ENABLE_SSH_LOG = true
+
+[log.test]
+LEVEL = Info
+COLORIZE = true
+
+[log.file]
+LEVEL = Debug
+
+[security]
+DISABLE_GIT_HOOKS = false
+INSTALL_LOCK = true
+SECRET_KEY = 9pCviYTWSb
+INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
+
+[lfs]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/lfs
+
+[packages]
+ENABLED = true
diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl
new file mode 100644
index 0000000000..f5e8895e06
--- /dev/null
+++ b/tests/sqlite.ini.tmpl
@@ -0,0 +1,109 @@
+APP_NAME = Gitea: Git with a cup of tea
+RUN_MODE = prod
+
+[database]
+DB_TYPE = sqlite3
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea.db
+
+[indexer]
+REPO_INDEXER_ENABLED = true
+REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/repos.bleve
+
+[queue.issue_indexer]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/issues.bleve
+DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/issues.queue
+
+[queue]
+TYPE = immediate
+
+[queue.code_indexer]
+TYPE = immediate
+
+[queue.push_update]
+TYPE = immediate
+
+[repository]
+ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea-repositories
+
+[repository.local]
+LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/tmp/local-repo
+
+[repository.upload]
+TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/tmp/uploads
+
+[repository.signing]
+SIGNING_KEY = none
+
+[server]
+SSH_DOMAIN = localhost
+HTTP_PORT = 3003
+ROOT_URL = http://localhost:3003/
+DISABLE_SSH = false
+SSH_LISTEN_HOST = localhost
+SSH_PORT = 2203
+START_SSH_SERVER = true
+LFS_START_SERVER = true
+OFFLINE_MODE = false
+LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
+APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data
+ENABLE_GZIP = true
+BUILTIN_SSH_SERVER_USER = git
+SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
+
+[attachment]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/attachments
+
+[mailer]
+ENABLED = true
+MAILER_TYPE = dummy
+FROM = sqlite-{{TEST_TYPE}}-test@gitea.io
+
+[service]
+REGISTER_EMAIL_CONFIRM = false
+REGISTER_MANUAL_CONFIRM = false
+ENABLE_NOTIFY_MAIL = true
+DISABLE_REGISTRATION = false
+ENABLE_CAPTCHA = false
+REQUIRE_SIGNIN_VIEW = false
+DEFAULT_KEEP_EMAIL_PRIVATE = false
+DEFAULT_ALLOW_CREATE_ORGANIZATION = true
+NO_REPLY_ADDRESS = noreply.example.org
+
+[picture]
+DISABLE_GRAVATAR = false
+ENABLE_FEDERATED_AVATAR = false
+AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/repo-avatars
+
+[session]
+PROVIDER = file
+PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/sessions
+
+[log]
+MODE = {{TEST_LOGGER}}
+ROOT_PATH = {{REPO_TEST_DIR}}sqlite-log
+ROUTER = ,
+XORM = file
+ENABLE_SSH_LOG = true
+
+[log.test]
+LEVEL = Info
+COLORIZE = true
+
+[log.file]
+LEVEL = Debug
+
+[security]
+DISABLE_GIT_HOOKS = false
+INSTALL_LOCK = true
+SECRET_KEY = 9pCviYTWSb
+INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTI3OTU5ODN9.OQkH5UmzID2XBdwQ9TAI6Jj2t1X-wElVTjbE7aoN4I8
+
+[oauth2]
+JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
+
+[lfs]
+PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
+
+[packages]
+ENABLED = true
diff --git a/tests/test_utils.go b/tests/test_utils.go
new file mode 100644
index 0000000000..0662530002
--- /dev/null
+++ b/tests/test_utils.go
@@ -0,0 +1,224 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package tests
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/queue"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func InitTest(requireGitea bool) {
+ giteaRoot := base.SetupGiteaRoot()
+ if giteaRoot == "" {
+ fmt.Println("Environment variable $GITEA_ROOT not set")
+ os.Exit(1)
+ }
+ if requireGitea {
+ giteaBinary := "gitea"
+ if runtime.GOOS == "windows" {
+ giteaBinary += ".exe"
+ }
+ setting.AppPath = path.Join(giteaRoot, giteaBinary)
+ if _, err := os.Stat(setting.AppPath); err != nil {
+ fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath)
+ os.Exit(1)
+ }
+ }
+
+ giteaConf := os.Getenv("GITEA_CONF")
+ if giteaConf == "" {
+ fmt.Println("Environment variable $GITEA_CONF not set")
+ os.Exit(1)
+ } else if !path.IsAbs(giteaConf) {
+ setting.CustomConf = path.Join(giteaRoot, giteaConf)
+ } else {
+ setting.CustomConf = giteaConf
+ }
+
+ setting.SetCustomPathAndConf("", "", "")
+ setting.LoadForTest()
+ setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
+ _ = util.RemoveAll(repo_module.LocalCopyPath())
+
+ if err := git.InitFull(context.Background()); err != nil {
+ log.Fatal("git.InitOnceWithSync: %v", err)
+ }
+
+ setting.InitDBConfig()
+ if err := storage.Init(); err != nil {
+ fmt.Printf("Init storage failed: %v", err)
+ os.Exit(1)
+ }
+
+ switch {
+ case setting.Database.UseMySQL:
+ connType := "tcp"
+ if len(setting.Database.Host) > 0 && setting.Database.Host[0] == '/' { // looks like a unix socket
+ connType = "unix"
+ }
+
+ db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@%s(%s)/",
+ setting.Database.User, setting.Database.Passwd, connType, setting.Database.Host))
+ defer db.Close()
+ if err != nil {
+ log.Fatal("sql.Open: %v", err)
+ }
+ if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", setting.Database.Name)); err != nil {
+ log.Fatal("db.Exec: %v", err)
+ }
+ case setting.Database.UsePostgreSQL:
+ var db *sql.DB
+ var err error
+ if setting.Database.Host[0] == '/' {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host))
+ } else {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
+ }
+
+ defer db.Close()
+ if err != nil {
+ log.Fatal("sql.Open: %v", err)
+ }
+ dbrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname = '%s'", setting.Database.Name))
+ if err != nil {
+ log.Fatal("db.Query: %v", err)
+ }
+ defer dbrows.Close()
+
+ if !dbrows.Next() {
+ if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", setting.Database.Name)); err != nil {
+ log.Fatal("db.Exec: CREATE DATABASE: %v", err)
+ }
+ }
+ // Check if we need to setup a specific schema
+ if len(setting.Database.Schema) == 0 {
+ break
+ }
+ db.Close()
+
+ if setting.Database.Host[0] == '/' {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@/%s?sslmode=%s&host=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Name, setting.Database.SSLMode, setting.Database.Host))
+ } else {
+ db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s",
+ setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode))
+ }
+ // This is a different db object; requires a different Close()
+ defer db.Close()
+ if err != nil {
+ log.Fatal("sql.Open: %v", err)
+ }
+ schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema))
+ if err != nil {
+ log.Fatal("db.Query: %v", err)
+ }
+ defer schrows.Close()
+
+ if !schrows.Next() {
+ // Create and setup a DB schema
+ if _, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s", setting.Database.Schema)); err != nil {
+ log.Fatal("db.Exec: CREATE SCHEMA: %v", err)
+ }
+ }
+
+ case setting.Database.UseMSSQL:
+ host, port := setting.ParseMSSQLHostPort(setting.Database.Host)
+ db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;",
+ host, port, "master", setting.Database.User, setting.Database.Passwd))
+ if err != nil {
+ log.Fatal("sql.Open: %v", err)
+ }
+ if _, err := db.Exec(fmt.Sprintf("If(db_id(N'%s') IS NULL) BEGIN CREATE DATABASE %s; END;", setting.Database.Name, setting.Database.Name)); err != nil {
+ log.Fatal("db.Exec: %v", err)
+ }
+ defer db.Close()
+ }
+
+ routers.GlobalInitInstalled(graceful.GetManager().HammerContext())
+}
+
+func PrepareTestEnv(t testing.TB, skip ...int) func() {
+ t.Helper()
+ ourSkip := 2
+ if len(skip) > 0 {
+ ourSkip += skip[0]
+ }
+ deferFn := PrintCurrentTest(t, ourSkip)
+ assert.NoError(t, unittest.LoadFixtures())
+ assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
+ assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
+ ownerDirs, err := os.ReadDir(setting.RepoRootPath)
+ if err != nil {
+ assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
+ }
+ for _, ownerDir := range ownerDirs {
+ if !ownerDir.Type().IsDir() {
+ continue
+ }
+ repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
+ if err != nil {
+ assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
+ }
+ for _, repoDir := range repoDirs {
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
+ }
+ }
+
+ return deferFn
+}
+
+// resetFixtures flushes queues, reloads fixtures and resets test repositories within a single test.
+// Most tests should call defer tests.PrepareTestEnv(t)() (or have onGiteaRun do that for them) but sometimes
+// within a single test this is required
+func ResetFixtures(t *testing.T) {
+ assert.NoError(t, queue.GetManager().FlushAll(context.Background(), -1))
+ assert.NoError(t, unittest.LoadFixtures())
+ assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
+ assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
+ ownerDirs, err := os.ReadDir(setting.RepoRootPath)
+ if err != nil {
+ assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
+ }
+ for _, ownerDir := range ownerDirs {
+ if !ownerDir.Type().IsDir() {
+ continue
+ }
+ repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
+ if err != nil {
+ assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
+ }
+ for _, repoDir := range repoDirs {
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
+ _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
+ }
+ }
+}
diff --git a/tests/testlogger.go b/tests/testlogger.go
new file mode 100644
index 0000000000..b65573a771
--- /dev/null
+++ b/tests/testlogger.go
@@ -0,0 +1,207 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package tests
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/queue"
+)
+
+var (
+ prefix string
+ SlowTest = 10 * time.Second
+ SlowFlush = 5 * time.Second
+)
+
+// TestLogger is a logger which will write to the testing log
+type TestLogger struct {
+ log.WriterLogger
+}
+
+var WriterCloser = &testLoggerWriterCloser{}
+
+type testLoggerWriterCloser struct {
+ sync.RWMutex
+ t []*testing.TB
+}
+
+func (w *testLoggerWriterCloser) setT(t *testing.TB) {
+ w.Lock()
+ w.t = append(w.t, t)
+ w.Unlock()
+}
+
+func (w *testLoggerWriterCloser) Write(p []byte) (int, error) {
+ w.RLock()
+ var t *testing.TB
+ if len(w.t) > 0 {
+ t = w.t[len(w.t)-1]
+ }
+ w.RUnlock()
+ if t != nil && *t != nil {
+ if len(p) > 0 && p[len(p)-1] == '\n' {
+ p = p[:len(p)-1]
+ }
+
+ defer func() {
+ err := recover()
+ if err == nil {
+ return
+ }
+ var errString string
+ errErr, ok := err.(error)
+ if ok {
+ errString = errErr.Error()
+ } else {
+ errString, ok = err.(string)
+ }
+ if !ok {
+ panic(err)
+ }
+ if !strings.HasPrefix(errString, "Log in goroutine after ") {
+ panic(err)
+ }
+ }()
+
+ (*t).Log(string(p))
+ return len(p), nil
+ }
+ return len(p), nil
+}
+
+func (w *testLoggerWriterCloser) Close() error {
+ w.Lock()
+ if len(w.t) > 0 {
+ w.t = w.t[:len(w.t)-1]
+ }
+ w.Unlock()
+ return nil
+}
+
+func (w *testLoggerWriterCloser) Reset() {
+ w.Lock()
+ if len(w.t) > 0 {
+ for _, t := range w.t {
+ if t == nil {
+ continue
+ }
+ fmt.Fprintf(os.Stdout, "Unclosed logger writer in test: %s", (*t).Name())
+ (*t).Errorf("Unclosed logger writer in test: %s", (*t).Name())
+ }
+ w.t = nil
+ }
+ w.Unlock()
+}
+
+// PrintCurrentTest prints the current test to os.Stdout
+func PrintCurrentTest(t testing.TB, skip ...int) func() {
+ start := time.Now()
+ actualSkip := 1
+ if len(skip) > 0 {
+ actualSkip = skip[0]
+ }
+ _, filename, line, _ := runtime.Caller(actualSkip)
+
+ if log.CanColorStdout {
+ fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", fmt.Formatter(log.NewColoredValue(t.Name())), strings.TrimPrefix(filename, prefix), line)
+ } else {
+ fmt.Fprintf(os.Stdout, "=== %s (%s:%d)\n", t.Name(), strings.TrimPrefix(filename, prefix), line)
+ }
+ WriterCloser.setT(&t)
+ return func() {
+ took := time.Since(start)
+ if took > SlowTest {
+ if log.CanColorStdout {
+ fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgYellow)), fmt.Formatter(log.NewColoredValue(took, log.Bold, log.FgYellow)))
+ } else {
+ fmt.Fprintf(os.Stdout, "+++ %s is a slow test (took %v)\n", t.Name(), took)
+ }
+ }
+ timer := time.AfterFunc(SlowFlush, func() {
+ if log.CanColorStdout {
+ fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), SlowFlush)
+ } else {
+ fmt.Fprintf(os.Stdout, "+++ %s ... still flushing after %v ...\n", t.Name(), SlowFlush)
+ }
+ })
+ if err := queue.GetManager().FlushAll(context.Background(), 2*time.Minute); err != nil {
+ t.Errorf("Flushing queues failed with error %v", err)
+ }
+ timer.Stop()
+ flushTook := time.Since(start) - took
+ if flushTook > SlowFlush {
+ if log.CanColorStdout {
+ fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", fmt.Formatter(log.NewColoredValue(t.Name(), log.Bold, log.FgRed)), fmt.Formatter(log.NewColoredValue(flushTook, log.Bold, log.FgRed)))
+ } else {
+ fmt.Fprintf(os.Stdout, "+++ %s had a slow clean-up flush (took %v)\n", t.Name(), flushTook)
+ }
+ }
+ _ = WriterCloser.Close()
+ }
+}
+
+// Printf takes a format and args and prints the string to os.Stdout
+func Printf(format string, args ...interface{}) {
+ if log.CanColorStdout {
+ for i := 0; i < len(args); i++ {
+ args[i] = log.NewColoredValue(args[i])
+ }
+ }
+ fmt.Fprintf(os.Stdout, "\t"+format, args...)
+}
+
+// NewTestLogger creates a TestLogger as a log.LoggerProvider
+func NewTestLogger() log.LoggerProvider {
+ logger := &TestLogger{}
+ logger.Colorize = log.CanColorStdout
+ logger.Level = log.TRACE
+ return logger
+}
+
+// Init inits connection writer with json config.
+// json config only need key "level".
+func (log *TestLogger) Init(config string) error {
+ err := json.Unmarshal([]byte(config), log)
+ if err != nil {
+ return err
+ }
+ log.NewWriterLogger(WriterCloser)
+ return nil
+}
+
+// Content returns the content accumulated in the content provider
+func (log *TestLogger) Content() (string, error) {
+ return "", fmt.Errorf("not supported")
+}
+
+// Flush when log should be flushed
+func (log *TestLogger) Flush() {
+}
+
+// ReleaseReopen does nothing
+func (log *TestLogger) ReleaseReopen() error {
+ return nil
+}
+
+// GetName returns the default name for this implementation
+func (log *TestLogger) GetName() string {
+ return "test"
+}
+
+func init() {
+ log.Register("test", NewTestLogger)
+ _, filename, _, _ := runtime.Caller(0)
+ prefix = strings.TrimSuffix(filename, "tests/integration/testlogger.go")
+}