From 2915bb27078a3eae0bac36bd2c3a2e1c4998130c Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Thu, 7 Sep 2017 09:21:52 +1100 Subject: [PATCH] Add API support for wiki pages --- changelogs/unreleased/wiki_api.yml | 5 + doc/api/README.md | 1 + doc/api/wikis.md | 157 +++++++ lib/api/api.rb | 1 + lib/api/entities.rb | 10 + lib/api/helpers.rb | 6 + lib/api/wikis.rb | 89 ++++ spec/requests/api/wikis_spec.rb | 679 +++++++++++++++++++++++++++++ 8 files changed, 948 insertions(+) create mode 100644 changelogs/unreleased/wiki_api.yml create mode 100644 doc/api/wikis.md create mode 100644 lib/api/wikis.rb create mode 100644 spec/requests/api/wikis_spec.rb diff --git a/changelogs/unreleased/wiki_api.yml b/changelogs/unreleased/wiki_api.yml new file mode 100644 index 00000000000..9d60356aedc --- /dev/null +++ b/changelogs/unreleased/wiki_api.yml @@ -0,0 +1,5 @@ +--- +title: Add API support for wiki pages +merge_request: 13372 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/doc/api/README.md b/doc/api/README.md index 266b5f018d9..f8e565ddbe5 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -58,6 +58,7 @@ following locations: - [Validate CI configuration](lint.md) - [V3 to V4](v3_to_v4.md) - [Version](version.md) +- [Wikis](wikis.md) ## Road to GraphQL diff --git a/doc/api/wikis.md b/doc/api/wikis.md new file mode 100644 index 00000000000..10eebc4a3c1 --- /dev/null +++ b/doc/api/wikis.md @@ -0,0 +1,157 @@ +# Wikis API + +> [Introduced][ce-13372] in GitLab 10.0. + +Available only in APIv4. + +## List wiki pages + +Get all wiki pages for a given project. + +``` +GET /projects/:id/wikis +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `with_content` | boolean | no | Include pages' content | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/wikis?with_content=1 +``` + +Example response: + +```json +[ + { + "content" : "Here is an instruction how to deploy this project.", + "format" : "markdown", + "slug" : "deploy", + "title" : "deploy" + }, + { + "content" : "Our development process is described here.", + "format" : "markdown", + "slug" : "development", + "title" : "development" + },{ + "content" : "* [Deploy](deploy)\n* [Development](development)", + "format" : "markdown", + "slug" : "home", + "title" : "home" + } +] +``` + +## Get a wiki page + +Get a wiki page for a given project. + +``` +GET /projects/:id/wikis/:slug +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `slug` | string | yes | The slug (a unique string) of the wiki page | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/wikis/home +``` + +Example response: + +```json +[ + { + "content" : "home page", + "format" : "markdown", + "slug" : "home", + "title" : "home" + } +] +``` + +## Create a new wiki page + +Creates a new wiki page for the given repository with the given title, slug, and content. + +``` +POST /projects/:id/wikis +``` + +| Attribute | Type | Required | Description | +| ------------- | ------- | -------- | ---------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `content` | string | yes | The content of the wiki page | +| `title` | string | yes | The title of the wiki page | +| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, and `asciidoc` | + +```bash +curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis" +``` + +Example response: + +```json +{ + "content" : "Hello world", + "format" : "markdown", + "slug" : "Hello", + "title" : "Hello" +} +``` + +## Edit an existing wiki page + +Updates an existing wiki page. At least one parameter is required to update the wiki page. + +``` +PUT /projects/:id/wikis/:slug +``` + +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `content` | string | yes if `title` is not provided | The content of the wiki page | +| `title` | string | yes if `content` is not provided | The title of the wiki page | +| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, and `asciidoc` | +| `slug` | string | yes | The slug (a unique string) of the wiki page | + + +```bash +curl --request PUT --data "format=rdoc&content=documentation&title=Docs" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo" +``` + +Example response: + +```json +{ + "content" : "documentation", + "format" : "markdown", + "slug" : "Docs", + "title" : "Docs" +} +``` + +## Delete a wiki page + +Deletes a wiki page with a given slug. + +``` +DELETE /projects/:id/wikis/:slug +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `slug` | string | yes | The slug (a unique string) of the wiki page | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo" +``` + +On success the HTTP status code is `204` and no JSON response is expected. diff --git a/lib/api/api.rb b/lib/api/api.rb index 94df543853b..9f67cb598f6 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -143,6 +143,7 @@ module API mount ::API::Variables mount ::API::GroupVariables mount ::API::Version + mount ::API::Wikis route :any, '*path' do error!('404 Not Found', 404) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e8dd61e493f..3c0fc4c2564 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1,5 +1,15 @@ module API module Entities + class WikiPageBasic < Grape::Entity + expose :format + expose :slug + expose :title + end + + class WikiPage < WikiPageBasic + expose :content + end + class UserSafe < Grape::Entity expose :name, :username end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index b56fd2388b3..0932f8eb76f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -35,6 +35,12 @@ module API @project ||= find_project!(params[:id]) end + def wiki_page + page = user_project.wiki.find_page(params[:slug]) + + page || not_found!('Wiki Page') + end + def available_labels @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb new file mode 100644 index 00000000000..b3fc4e876ad --- /dev/null +++ b/lib/api/wikis.rb @@ -0,0 +1,89 @@ +module API + class Wikis < Grape::API + helpers do + params :wiki_page_params do + requires :content, type: String, desc: 'Content of a wiki page' + requires :title, type: String, desc: 'Title of a wiki page' + optional :format, + type: String, + values: ProjectWiki::MARKUPS.values.map(&:to_s), + default: 'markdown', + desc: 'Format of a wiki page. Available formats are markdown, rdoc, and asciidoc' + end + end + + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + desc 'Get a list of wiki pages' do + success Entities::WikiPageBasic + end + params do + optional :with_content, type: Boolean, default: false, desc: "Include pages' content" + end + get ':id/wikis' do + authorize! :read_wiki, user_project + + entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic + present user_project.wiki.pages, with: entity + end + + desc 'Get a wiki page' do + success Entities::WikiPage + end + params do + requires :slug, type: String, desc: 'The slug of a wiki page' + end + get ':id/wikis/:slug' do + authorize! :read_wiki, user_project + + present wiki_page, with: Entities::WikiPage + end + + desc 'Create a wiki page' do + success Entities::WikiPage + end + params do + use :wiki_page_params + end + post ':id/wikis' do + authorize! :create_wiki, user_project + + page = WikiPages::CreateService.new(user_project, current_user, params).execute + + if page.valid? + present page, with: Entities::WikiPage + else + render_validation_error!(page) + end + end + + desc 'Update a wiki page' do + success Entities::WikiPage + end + params do + use :wiki_page_params + end + put ':id/wikis/:slug' do + authorize! :create_wiki, user_project + + page = WikiPages::UpdateService.new(user_project, current_user, params).execute(wiki_page) + + if page.valid? + present page, with: Entities::WikiPage + else + render_validation_error!(page) + end + end + + desc 'Delete a wiki page' + params do + requires :slug, type: String, desc: 'The slug of a wiki page' + end + delete ':id/wikis/:slug' do + authorize! :admin_wiki, user_project + + status 204 + WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page) + end + end + end +end diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb new file mode 100644 index 00000000000..9e889d1eecf --- /dev/null +++ b/spec/requests/api/wikis_spec.rb @@ -0,0 +1,679 @@ +require 'spec_helper' + +# For every API endpoint we test 3 states of wikis: +# - disabled +# - enabled only for team members +# - enabled for everyone who has access +# Every state is tested for 3 user roles: +# - guest +# - developer +# - master +# because they are 3 edge cases of using wiki pages. + +describe API::Wikis do + let(:user) { create(:user) } + let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } } + let(:expected_keys_with_content) { %w(content format slug title) } + let(:expected_keys_without_content) { %w(format slug title) } + + shared_examples_for 'returns list of wiki pages' do + context 'when wiki has pages' do + let!(:pages) do + [create(:wiki_page, wiki: project.wiki, attrs: { title: 'page1', content: 'content of page1' }), + create(:wiki_page, wiki: project.wiki, attrs: { title: 'page2', content: 'content of page2' })] + end + + it 'returns the list of wiki pages without content' do + get api(url, user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_without_content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + + it 'returns the list of wiki pages with content' do + get api(url, user), with_content: 1 + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_with_content) + expect(page['content']).to eq(pages[index].content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + end + + it 'return the empty list of wiki pages' do + get api(url, user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + end + + shared_examples_for 'returns wiki page' do + it 'returns the wiki page' do + expect(response).to have_http_status(200) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(page.content) + expect(json_response['slug']).to eq(page.slug) + expect(json_response['title']).to eq(page.title) + end + end + + shared_examples_for 'creates wiki page' do + it 'creates the wiki page' do + post(api(url, user), payload) + + expect(response).to have_http_status(201) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + expect(json_response['rdoc']).to eq(payload[:rdoc]) + end + + [:title, :content].each do |part| + it "responds with validation error on empty #{part}" do + payload.delete(part) + + post(api(url, user), payload) + + expect(response).to have_http_status(400) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq("#{part} is missing") + end + end + end + + shared_examples_for 'updates wiki page' do + it 'updates the wiki page' do + expect(response).to have_http_status(200) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + end + end + + shared_examples_for '403 Forbidden' do + it 'returns 403 Forbidden' do + expect(response).to have_http_status(403) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('403 Forbidden') + end + end + + shared_examples_for '404 Wiki Page Not Found' do + it 'returns 404 Wiki Page Not Found' do + expect(response).to have_http_status(404) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('404 Wiki Page Not Found') + end + end + + shared_examples_for '404 Project Not Found' do + it 'returns 404 Project Not Found' do + expect(response).to have_http_status(404) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + shared_examples_for '204 No Content' do + it 'returns 204 No Content' do + expect(response).to have_http_status(204) + end + end + + describe 'GET /projects/:id/wikis' do + let(:url) { "/projects/#{project.id}/wikis" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'returns list of wiki pages' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'returns list of wiki pages' + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'returns list of wiki pages' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'returns list of wiki pages' + end + end + end + + describe 'GET /projects/:id/wikis/:slug' do + let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + end + + describe 'POST /projects/:id/wikis' do + let(:payload) { { title: 'title', content: 'content' } } + let(:url) { "/projects/#{project.id}/wikis" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + post(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + post(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'creates wiki page' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'creates wiki page' + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'creates wiki page' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'creates wiki page' + end + end + end + + describe 'PUT /projects/:id/wikis/:slug' do + let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:payload) { { title: 'new title', content: 'new content' } } + let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + put(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + put(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + put(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + put(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + put(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + end + + describe 'DELETE /projects/:id/wikis/:slug' do + let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + delete(api(url)) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + delete(api(url)) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + delete(api(url, user)) + end + + include_examples '204 No Content' + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + delete(api(url)) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + delete(api(url, user)) + end + + include_examples '204 No Content' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + end +end