wordpress_client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.hound.yml +2 -0
  4. data/.rubocop.yml +1065 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +11 -0
  7. data/Guardfile +29 -0
  8. data/LICENSE +21 -0
  9. data/README.md +63 -0
  10. data/Rakefile +1 -0
  11. data/circle.yml +3 -0
  12. data/lib/wordpress_client.rb +25 -0
  13. data/lib/wordpress_client/category.rb +4 -0
  14. data/lib/wordpress_client/client.rb +142 -0
  15. data/lib/wordpress_client/connection.rb +186 -0
  16. data/lib/wordpress_client/errors.rb +10 -0
  17. data/lib/wordpress_client/media.rb +37 -0
  18. data/lib/wordpress_client/media_parser.rb +48 -0
  19. data/lib/wordpress_client/paginated_collection.rb +53 -0
  20. data/lib/wordpress_client/post.rb +62 -0
  21. data/lib/wordpress_client/post_parser.rb +113 -0
  22. data/lib/wordpress_client/replace_metadata.rb +81 -0
  23. data/lib/wordpress_client/replace_terms.rb +62 -0
  24. data/lib/wordpress_client/rest_parser.rb +17 -0
  25. data/lib/wordpress_client/tag.rb +4 -0
  26. data/lib/wordpress_client/term.rb +34 -0
  27. data/lib/wordpress_client/version.rb +3 -0
  28. data/spec/category_spec.rb +8 -0
  29. data/spec/client_spec.rb +411 -0
  30. data/spec/connection_spec.rb +270 -0
  31. data/spec/docker/Dockerfile +40 -0
  32. data/spec/docker/README.md +37 -0
  33. data/spec/docker/dbdump.sql.gz +0 -0
  34. data/spec/docker/htaccess +10 -0
  35. data/spec/docker/restore-dbdump.sh +13 -0
  36. data/spec/fixtures/category.json +1 -0
  37. data/spec/fixtures/image-media.json +1 -0
  38. data/spec/fixtures/invalid-post-id.json +1 -0
  39. data/spec/fixtures/post-with-forbidden-metadata.json +1 -0
  40. data/spec/fixtures/post-with-metadata.json +1 -0
  41. data/spec/fixtures/simple-post.json +1 -0
  42. data/spec/fixtures/tag.json +1 -0
  43. data/spec/fixtures/thoughtful.jpg +0 -0
  44. data/spec/fixtures/validation-error.json +1 -0
  45. data/spec/integration/attachments_crud_spec.rb +51 -0
  46. data/spec/integration/categories_spec.rb +60 -0
  47. data/spec/integration/category_assignment_spec.rb +29 -0
  48. data/spec/integration/posts_crud_spec.rb +118 -0
  49. data/spec/integration/posts_finding_spec.rb +86 -0
  50. data/spec/integration/posts_metadata_spec.rb +27 -0
  51. data/spec/integration/posts_with_attachments_spec.rb +21 -0
  52. data/spec/integration/tag_assignment_spec.rb +29 -0
  53. data/spec/integration/tags_spec.rb +36 -0
  54. data/spec/media_spec.rb +63 -0
  55. data/spec/paginated_collection_spec.rb +64 -0
  56. data/spec/post_spec.rb +114 -0
  57. data/spec/replace_metadata_spec.rb +56 -0
  58. data/spec/replace_terms_spec.rb +51 -0
  59. data/spec/shared_examples/term_examples.rb +37 -0
  60. data/spec/spec_helper.rb +28 -0
  61. data/spec/support/docker_runner.rb +49 -0
  62. data/spec/support/fixtures.rb +19 -0
  63. data/spec/support/integration_macros.rb +10 -0
  64. data/spec/support/wordpress_server.rb +103 -0
  65. data/spec/tag_spec.rb +8 -0
  66. data/wordpress_client.gemspec +27 -0
  67. metadata +219 -0
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+
3
+ module WordpressClient
4
+ describe PaginatedCollection do
5
+ it "wraps an array" do
6
+ list = ["one"]
7
+ pagination = PaginatedCollection.new(list, total: 1, per_page: 1, current_page: 1)
8
+
9
+ expect(pagination.each.to_a).to eq list
10
+ expect(pagination.to_a).to eq list
11
+ expect(pagination.size).to eq 1
12
+
13
+ expect(pagination.total).to eq 1
14
+ expect(pagination.per_page).to eq 1
15
+ expect(pagination.current_page).to eq 1
16
+ end
17
+
18
+ describe "pagination attributes" do
19
+ def collection(total: 1, per_page: 1, current_page: 1)
20
+ PaginatedCollection.new([], total: total, per_page: per_page, current_page: current_page)
21
+ end
22
+
23
+ it "includes total pages" do
24
+ expect(collection(total: 10, per_page: 5).total_pages).to eq 2
25
+ expect(collection(total: 11, per_page: 5).total_pages).to eq 3
26
+ expect(collection(total: 0, per_page: 5).total_pages).to eq 0
27
+ expect(collection(total: 10, per_page: 0).total_pages).to eq 0
28
+ expect(collection(total: 2, per_page: 9).total_pages).to eq 1
29
+ end
30
+
31
+ it "includes next page number" do
32
+ expect(collection(total: 10, per_page: 1, current_page: 1).next_page).to eq 2
33
+ expect(collection(total: 10, per_page: 1, current_page: 9).next_page).to eq 10
34
+ expect(collection(total: 10, per_page: 1, current_page: 10).next_page).to eq nil
35
+
36
+ expect(collection(total: 10, per_page: 0, current_page: 1).next_page).to eq nil
37
+ end
38
+
39
+ it "includes previous page number" do
40
+ expect(collection(total: 10, per_page: 1, current_page: 10).previous_page).to eq 9
41
+ expect(collection(total: 10, per_page: 1, current_page: 2).previous_page).to eq 1
42
+ expect(collection(total: 10, per_page: 1, current_page: 1).previous_page).to eq nil
43
+ end
44
+
45
+ it "is out of bounds if current page is after last page" do
46
+ expect(collection(total: 2, per_page: 1, current_page: 1)).to_not be_out_of_bounds
47
+ expect(collection(total: 2, per_page: 1, current_page: 2)).to_not be_out_of_bounds
48
+
49
+ expect(collection(total: 2, per_page: 1, current_page: 3)).to be_out_of_bounds
50
+ expect(collection(total: 2, per_page: 1, current_page: 0)).to be_out_of_bounds
51
+ end
52
+
53
+ # Only to be compatible with will_paginate < 3.0
54
+ it "has an offset" do
55
+ expect(collection(per_page: 5, current_page: 1).offset).to eq 0
56
+ expect(collection(per_page: 5, current_page: 2).offset).to eq 5
57
+ expect(collection(per_page: 50, current_page: 3).offset).to eq 100
58
+
59
+ expect(collection(per_page: 50, current_page: 0).offset).to eq 0
60
+ expect(collection(per_page: 0, current_page: 0).offset).to eq 0
61
+ end
62
+ end
63
+ end
64
+ end
data/spec/post_spec.rb ADDED
@@ -0,0 +1,114 @@
1
+ require "spec_helper"
2
+
3
+ module WordpressClient
4
+ describe Post do
5
+ let(:fixture) { json_fixture("simple-post.json") }
6
+
7
+ it "can be parsed from JSON data" do
8
+ post = Post.parse(fixture)
9
+
10
+ expect(post.id).to eq 1
11
+ expect(post.title_html).to eq "Hello world!"
12
+ expect(post.slug).to eq "hello-world"
13
+
14
+ expect(post.url).to eq "http://example.com/2015/11/03/hello-world/"
15
+ expect(post.guid).to eq "http://example.com/?p=1"
16
+
17
+ expect(post.excerpt_html).to eq(
18
+ "<p>Welcome to WordPress. This is your first post. Edit or delete it, then start " \
19
+ "writing!</p>\n"
20
+ )
21
+
22
+ expect(post.content_html).to eq(
23
+ "<p>Welcome to WordPress. This is your first post. Edit or delete it, then start " \
24
+ "writing!</p>\n"
25
+ )
26
+
27
+ expect(post.date).to_not be nil
28
+ expect(post.updated_at).to_not be nil
29
+ end
30
+
31
+ it "parses categories" do
32
+ post = Post.parse(fixture)
33
+
34
+ expect(post.categories).to eq [
35
+ Category.new(
36
+ id: 1, name_html: "Uncategorized", slug: "uncategorized"
37
+ )
38
+ ]
39
+
40
+ expect(post.category_ids).to eq [1]
41
+ end
42
+
43
+ it "parses tags" do
44
+ post = Post.parse(fixture)
45
+
46
+ expect(post.tags).to eq [
47
+ Tag.new(
48
+ id: 2, name_html: "Foo", slug: "foo"
49
+ )
50
+ ]
51
+
52
+ expect(post.tag_ids).to eq [2]
53
+ end
54
+
55
+ it "can have a Media as featured image" do
56
+ media = instance_double(Media, id: 12)
57
+ post = Post.new(featured_image: media)
58
+
59
+ expect(post.featured_image).to eq media
60
+ expect(post.featured_image_id).to eq 12
61
+ end
62
+
63
+ describe "dates" do
64
+ it "uses GMT times if available" do
65
+ post = Post.parse(fixture.merge(
66
+ "date_gmt" => "2001-01-01T15:00:00",
67
+ "date" => "2001-01-01T12:00:00",
68
+ "modified_gmt" => "2001-01-01T15:00:00",
69
+ "modified" => "2001-01-01T12:00:00",
70
+ ))
71
+
72
+ expect(post.date).to eq Time.utc(2001, 1, 1, 15, 0, 0)
73
+ expect(post.updated_at).to eq Time.utc(2001, 1, 1, 15, 0, 0)
74
+ end
75
+
76
+ it "falls back to local time if no GMT date is provided" do
77
+ post = Post.parse(fixture.merge(
78
+ "date_gmt" => nil,
79
+ "date" => "2001-01-01T12:00:00",
80
+ "modified_gmt" => nil,
81
+ "modified" => "2001-01-01T12:00:00",
82
+ ))
83
+
84
+ expect(post.date).to eq Time.local(2001, 1, 1, 12, 0, 0)
85
+ expect(post.updated_at).to eq Time.local(2001, 1, 1, 12, 0, 0)
86
+ end
87
+ end
88
+
89
+ describe "metadata" do
90
+ it "is parsed into a hash" do
91
+ post = Post.parse(json_fixture("post-with-metadata.json"))
92
+ expect(post.meta).to eq "foo" => "bar"
93
+ end
94
+
95
+ it "raises UnauthorizedError when post it is forbidden" do
96
+ expect {
97
+ Post.parse(json_fixture("post-with-forbidden-metadata.json"))
98
+ }.to raise_error(UnauthorizedError)
99
+ end
100
+
101
+ it "keeps track of the ID of each metadata key" do
102
+ post = Post.parse(json_fixture("post-with-metadata.json"))
103
+ expect(post.meta_id_for("foo")).to eq 2
104
+ end
105
+
106
+ it "raises ArgumentError when asked for the meta ID of a meta key not present" do
107
+ post = Post.parse(json_fixture("post-with-metadata.json"))
108
+ expect {
109
+ post.meta_id_for("clearly unreal")
110
+ }.to raise_error(ArgumentError, /clearly unreal/)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,56 @@
1
+ require "spec_helper"
2
+
3
+ module WordpressClient
4
+ describe ReplaceMetadata do
5
+ it "does nothing if the new metadata is equal to the existing one" do
6
+ post = instance_double(Post, id: 5, meta: {"existing" => "1"})
7
+
8
+ # Note: connection double does not accept any message.
9
+ connection = instance_double(Connection)
10
+
11
+ ReplaceMetadata.apply(connection, post, existing: "1")
12
+ end
13
+
14
+ it "adds missing metadata" do
15
+ connection = instance_double(Connection)
16
+ post = instance_double(Post, id: 5, meta: {"existing" => "1"})
17
+
18
+ expect(connection).to receive(:create_without_response).with(
19
+ "posts/5/meta", key: "new", value: "2"
20
+ )
21
+
22
+ ReplaceMetadata.apply(connection, post, existing: "1", new: "2")
23
+ end
24
+
25
+ it "replaces changed metadata" do
26
+ connection = instance_double(Connection)
27
+ post = instance_double(Post, id: 5, meta: {"change_me" => "1"})
28
+
29
+ expect(post).to receive(:meta_id_for).with("change_me").and_return(13)
30
+
31
+ expect(connection).to receive(:patch_without_response).with(
32
+ "posts/5/meta/13", key: "change_me", value: "2"
33
+ )
34
+
35
+ ReplaceMetadata.apply(connection, post, change_me: "2")
36
+ end
37
+
38
+ it "removes extra metadata" do
39
+ connection = instance_double(Connection)
40
+ post = instance_double(Post, id: 5, meta: {"old" => "1", "new" => "2"})
41
+
42
+ expect(post).to receive(:meta_id_for).with("old").and_return(45)
43
+ expect(connection).to receive(:delete).with("posts/5/meta/45", force: true)
44
+
45
+ ReplaceMetadata.apply(connection, post, new: "2")
46
+ end
47
+
48
+ it "returns the number of changes" do
49
+ connection = instance_double(Connection).as_null_object
50
+ post = instance_double(Post, id: 5, meta: {"old" => "1", "change" => "2"}).as_null_object
51
+
52
+ result = ReplaceMetadata.apply(connection, post, change: "3", extra: "4")
53
+ expect(result).to eq 3
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ module WordpressClient
4
+ describe ReplaceTerms do
5
+ it "adds missing categories" do
6
+ connection = double(Connection)
7
+ post = double(Post, id: 40, category_ids: [1])
8
+
9
+ expect(connection).to receive(:create_without_response).with("posts/40/terms/category/5", {})
10
+
11
+ ReplaceTerms.apply_categories(connection, post, [1, 5])
12
+ end
13
+
14
+ it "removes extra categories" do
15
+ connection = double(Connection)
16
+ post = double(Post, id: 40, category_ids: [8, 9, 10])
17
+
18
+ expect(connection).to receive(:delete).with("posts/40/terms/category/8", force: true)
19
+ expect(connection).to receive(:delete).with("posts/40/terms/category/9", force: true)
20
+
21
+ ReplaceTerms.apply_categories(connection, post, [10])
22
+ end
23
+
24
+ it "adds missing tags" do
25
+ connection = double(Connection)
26
+ post = double(Post, id: 40, tag_ids: [1])
27
+
28
+ expect(connection).to receive(:create_without_response).with("posts/40/terms/tag/5", {})
29
+
30
+ ReplaceTerms.apply_tags(connection, post, [1, 5])
31
+ end
32
+
33
+ it "removes extra tags" do
34
+ connection = double(Connection)
35
+ post = double(Post, id: 40, tag_ids: [8, 9, 10])
36
+
37
+ expect(connection).to receive(:delete).with("posts/40/terms/tag/8", force: true)
38
+ expect(connection).to receive(:delete).with("posts/40/terms/tag/9", force: true)
39
+
40
+ ReplaceTerms.apply_tags(connection, post, [10])
41
+ end
42
+
43
+ it "returns the amount of changes made" do
44
+ connection = double(Connection).as_null_object
45
+ post = double(Post, id: 40, tag_ids: [8, 9, 10])
46
+
47
+ result = ReplaceTerms.apply_tags(connection, post, [10, 11])
48
+ expect(result).to eq 3
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,37 @@
1
+ shared_examples_for(WordpressClient::Term) do |fixture_name:|
2
+ it "has an id, name_html and slug" do
3
+ term = described_class.new(id: 5, name_html: "Heyho", slug: "heyho")
4
+ expect(term.id).to eq 5
5
+ expect(term.name_html).to eq "Heyho"
6
+ expect(term.slug).to eq "heyho"
7
+ end
8
+
9
+ it "can be parsed" do
10
+ term = described_class.parse(json_fixture(fixture_name))
11
+ expect(term.id).to be_kind_of Integer
12
+
13
+ expect(term.name_html).to be_kind_of String
14
+ expect(term.slug).to be_kind_of String
15
+ expect(term.name_html).to_not be_empty
16
+ expect(term.slug).to_not be_empty
17
+ end
18
+
19
+ it "is equal to other instances with the same id, name_html and slug" do
20
+ instance = described_class.new(id: 1, name_html: "One", slug: "one")
21
+ copy = described_class.new(id: 1, name_html: "One", slug: "one")
22
+
23
+ expect(instance).to eq instance
24
+ expect(instance).to eq copy
25
+
26
+ expect(instance).to_not eq described_class.new(id: 2, name_html: "One", slug: "one")
27
+ expect(instance).to_not eq described_class.new(id: 1, name_html: "Two", slug: "one")
28
+ expect(instance).to_not eq described_class.new(id: 1, name_html: "One", slug: "two")
29
+ end
30
+
31
+ it "it not equal on other Term subclasses with the same id, name_html and slug" do
32
+ other_subclass = Class.new(WordpressClient::Tag)
33
+
34
+ term = described_class.new(id: 1, name_html: "One", slug: "one")
35
+ expect(term).to_not eq other_subclass.new(id: 1, name_html: "One", slug: "one")
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ if ENV['CODECLIMATE_REPO_TOKEN']
2
+ require "codeclimate-test-reporter"
3
+ CodeClimate::TestReporter.start
4
+ end
5
+
6
+ require "webmock/rspec"
7
+
8
+ require_relative "support/wordpress_server"
9
+ require_relative "support/fixtures"
10
+ require_relative "support/integration_macros"
11
+
12
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
13
+ require "wordpress_client"
14
+
15
+ RSpec.configure do |config|
16
+ config.extend IntegrationMacros
17
+ config.include Fixtures
18
+
19
+ config.run_all_when_everything_filtered = true
20
+
21
+ config.before do
22
+ WebMock.disable_net_connect!(allow_localhost: false)
23
+ end
24
+
25
+ config.after do
26
+ WebMock.allow_net_connect!
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ require "shellwords"
2
+ require "uri"
3
+
4
+ module DockerRunner
5
+ extend self
6
+
7
+ def docker_installed?
8
+ system("hash docker > /dev/null 2> /dev/null")
9
+ end
10
+
11
+ def image_exists?(name)
12
+ system("docker images | grep -q '^#{name.shellescape} '")
13
+ end
14
+
15
+ def build_image(name, path: Dir.pwd)
16
+ system("cd #{path.shellescape} && docker build -t #{name.shellescape} .")
17
+ end
18
+
19
+ def run_container(name, port:, environment: {})
20
+ environment_flags = environment.map { |key, value|
21
+ "-e #{key.to_s.upcase.shellescape}=#{value.shellescape}"
22
+ }
23
+
24
+ output = `
25
+ docker run \
26
+ -dit -p #{port.to_i}:80 \
27
+ #{environment_flags.join(" ")} \
28
+ #{name.shellescape}
29
+ `
30
+ if $?.success?
31
+ output.chomp
32
+ else
33
+ fail "Failed to start container. Maybe it's already running? Output: #{output}"
34
+ end
35
+ end
36
+
37
+ def purge_container(id)
38
+ output = `docker kill #{id.shellescape}; docker rm #{id.shellescape} `
39
+
40
+ unless $?.success?
41
+ message = "Could not clean up docker image #{id}. Output was:\n#{output}.\n"
42
+ if ENV["CIRCLECI"]
43
+ puts message
44
+ else
45
+ raise message
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,19 @@
1
+ module Fixtures
2
+ FixtureRoot = Pathname.new(File.expand_path("../../fixtures", __FILE__))
3
+
4
+ def fixture_path(name)
5
+ FixtureRoot.join(name)
6
+ end
7
+
8
+ def fixture_contents(name)
9
+ fixture_path(name).read
10
+ end
11
+
12
+ def json_fixture(name)
13
+ JSON.parse(fixture_contents(name))
14
+ end
15
+
16
+ def open_fixture(name)
17
+ fixture_path(name).open('r') { |file| yield file }
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module IntegrationMacros
2
+ def setup_integration_client
3
+ before { WebMock.allow_net_connect! }
4
+
5
+ let(:client) {
6
+ server = WordpressServer.instance
7
+ WordpressClient.new(url: server.url, username: server.username, password: server.password)
8
+ }
9
+ end
10
+ end
@@ -0,0 +1,103 @@
1
+ require_relative "docker_runner"
2
+
3
+ class WordpressServer
4
+ include Singleton
5
+
6
+ attr_reader :container_id, :port, :host
7
+
8
+ def initialize
9
+ @host = docker_host
10
+ @port = 8181
11
+
12
+ start_docker_container
13
+ at_exit { purge_container }
14
+ end
15
+
16
+ def url
17
+ "http://#{host_with_port}/wp-json"
18
+ end
19
+
20
+ # Defined in the dbdump in spec/docker/dbdump.sql.gz
21
+ def username() "test" end
22
+
23
+ # Defined in the dbdump in spec/docker/dbdump.sql.gz
24
+ def password() "test" end
25
+
26
+ private
27
+ def host_with_port
28
+ "#{host}:#{port}"
29
+ end
30
+
31
+ def docker_host
32
+ if ENV['DOCKER_HOST']
33
+ URI.parse(ENV['DOCKER_HOST']).host
34
+ else
35
+ "localhost"
36
+ end
37
+ end
38
+
39
+ def start_docker_container
40
+ fail_if_docker_missing
41
+ build_container_if_missing
42
+
43
+ @container_id = start_container
44
+ @running = true
45
+
46
+ begin
47
+ wait_for_container_to_start
48
+ rescue
49
+ purge_container
50
+ raise $!
51
+ end
52
+ end
53
+
54
+ def fail_if_docker_missing
55
+ unless DockerRunner.docker_installed?
56
+ STDERR.puts(
57
+ "It does not look like you have docker installed. " \
58
+ "Please install docker so you can run integration tests."
59
+ )
60
+ fail "No docker installed"
61
+ end
62
+ end
63
+
64
+ def build_container_if_missing
65
+ unless DockerRunner.image_exists?("wpclient-test")
66
+ DockerRunner.build_image("wpclient-test", path: "spec/docker")
67
+ end
68
+ end
69
+
70
+ def start_container
71
+ DockerRunner.run_container(
72
+ "wpclient-test",
73
+ port: port,
74
+ environment: {wordpress_host: host_with_port}
75
+ )
76
+ end
77
+
78
+ def purge_container
79
+ if @running
80
+ DockerRunner.purge_container(container_id)
81
+ @running = true
82
+ end
83
+ end
84
+
85
+ def wait_for_container_to_start
86
+ # Try to connect to the webserver in a loop until we successfully connect,
87
+ # the container process dies, or the timeout is reached.
88
+ timeout = 60
89
+ start = Time.now
90
+
91
+ loop do
92
+ fail "Timed out while waiting for the container to start" if Time.now - start > timeout
93
+
94
+ begin
95
+ response = Faraday.get(url)
96
+ return if response.status == 200
97
+ rescue Faraday::ConnectionFailed
98
+ # Server not yet started. Just wait it out...
99
+ end
100
+ sleep 0.5
101
+ end
102
+ end
103
+ end