wordpress_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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