spec/service_spec.rb in ruby_odata-0.0.10 vs spec/service_spec.rb in ruby_odata-0.1.0

- old
+ new

@@ -35,11 +35,10 @@ svc = OData::Service.new "http://test.com/test.svc/", { :additional_params => { :x => 1, :y => 2 } } a_request(:get, "http://test.com/test.svc/$metadata?x=1&y=2").should have_been_made end end end - describe "additional query string parameters" do before(:each) do # Required for the build_classes method stub_request(:any, /http:\/\/test\.com\/test\.svc(?:.*)/). with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). @@ -81,11 +80,31 @@ new_flight2 = ZDemoFlight.new svc.AddToflight_dataCollection(new_flight2) svc.save_changes a_request(:post, "http://test.com/test.svc/$batch?x=1&y=2").should have_been_made end - end + it "should pass the parameters as part of an add link" do + svc = OData::Service.new "http://test.com/test.svc/", { :additional_params => { :x=>1, :y=>2 } } + existing_flight1 = ZDemoFlight.new + existing_flight1.__metadata = { :uri => "http://test.com/test.svc/flight_dataCollection/1" } + existing_flight2 = ZDemoFlight.new + existing_flight2.__metadata = { :uri => "http://test.com/test.svc/flight_dataCollection/2" } + svc.add_link(existing_flight1, "flight_data_r", existing_flight2) + svc.save_changes + a_request(:post, "http://test.com/test.svc/flight_dataCollection/1/$links/flight_data_r?x=1&y=2").should have_been_made + end + it "should pass the parameters as part of a function import with a parameter" do + svc = OData::Service.new "http://test.com/test.svc/", { :additional_params => { :x=>1, :y=>2 } } + svc.get_flight(1) + a_request(:get, "http://test.com/test.svc/get_flight?id=1&x=1&y=2").should have_been_made + end + it "should pass the parameters as part of a function import without parameters" do + svc = OData::Service.new "http://test.com/test.svc/", { :additional_params => { :x=>1, :y=>2 } } + svc.get_top_flight + a_request(:get, "http://test.com/test.svc/get_top_flight?x=1&y=2").should have_been_made + end + end describe "lowercase collections" do before(:each) do # Required for the build_classes method stub_request(:get, "http://test.com/test.svc/$metadata"). @@ -122,11 +141,11 @@ results = svc.execute results.first.should be_a_kind_of(ZDemoFlight) end end - describe "collections, objects, etc" do + describe "collections, objects, metadata etc" do before(:each) do # Metadata stub_request(:get, "http://test.com/test.svc/$metadata"). with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/feed_customization/edmx_feed_customization.xml", __FILE__)), :headers => {}) @@ -242,12 +261,452 @@ results = svc.execute product = results.first product.Category.should_not be_an Array end end + + describe "navigation properties" do + it "should fill in PropertyMetadata for navigation properties" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.class_metadata['Product'].should have_key 'Category' + end + end end - end + describe "single layer inheritance" do + before(:each) do + # Metadata + stub_request(:get, "http://test.com/test.svc/$metadata"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/inheritance/edmx_pluralsight.xml", __FILE__)), :headers => {}) + + # Content - Courses + stub_request(:get, /http:\/\/test\.com\/test\.svc\/Courses(?:.*)/). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/inheritance/result_pluralsight_courses.xml", __FILE__)), :headers => {}) + end + + it "should build all inherited attributes" do + OData::Service.new "http://test.com/test.svc/" + methods = Course.instance_methods.reject {|m| Object.methods.index(m)} + + # Ruby 1.9 uses symbols, and 1.8 uses strings, so this normalizes the data + methods.map! {|m| m.to_sym} + + methods.should include(:Title) + methods.should include(:Description) + methods.should include(:VideoLength) + methods.should include(:Category) + + methods.should include(:Title=) + methods.should include(:Description=) + methods.should include(:VideoLength=) + methods.should include(:Category=) + end + + it "should not build abstract classes" do + OData::Service.new "http://test.com/test.svc/" + defined?(ModelItemBase).should eq nil + end + + it "should fill inherited properties" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Courses + courses = svc.execute + course = courses.first + course.Title.should_not be_nil + course.Description.should_not be_nil + course.VideoLength.should_not be_nil + course.Category.should_not be_nil + end + end + + describe "handling partial collections" do + before(:each) do + # Metadata + stub_request(:get, "http://test.com/test.svc/$metadata"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/partial/partial_feed_metadata.xml", __FILE__)), :headers => {}) + + # Content - Partial + stub_request(:get, "http://test.com/test.svc/Partials"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/partial/partial_feed_part_1.xml", __FILE__)), :headers => {}) + + stub_request(:get, "http://test.com/test.svc/Partials?$skiptoken='ERNSH'"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/partial/partial_feed_part_2.xml", __FILE__)), :headers => {}) + + stub_request(:get, "http://test.com/test.svc/Partials?$skiptoken='ERNSH2'"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/partial/partial_feed_part_3.xml", __FILE__)), :headers => {}) + + end + + it "should return the whole collection by default" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Partials + results = svc.execute + results.count.should eq 3 + end + + it "should return only the partial when specified by options" do + svc = OData::Service.new("http://test.com/test.svc/", :eager_partial => false) + svc.Partials + results = svc.execute + results.count.should eq 1 + svc.should be_partial + while svc.partial? + results.concat svc.next + end + results.count.should eq 3 + end + end + + describe "link queries" do + before(:each) do + # Required for the build_classes method + stub_request(:get, /http:\/\/test\.com\/test\.svc\/\$metadata(?:\?.+)?/). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/sample_service/edmx_categories_products.xml", __FILE__)), :headers => {}) + + stub_request(:get, "http://test.com/test.svc/Categories(1)/$links/Products"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/links/result_links_query.xml", __FILE__)), :headers => {}) + end + it "should be able to parse the results of a links query" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1).links('Products') + results = svc.execute + results.count.should eq 3 + results.first.should be_a_kind_of(URI) + results[0].path.should eq "/SampleService/RubyOData.svc/Products(1)" + results[1].path.should eq "/SampleService/RubyOData.svc/Products(2)" + results[2].path.should eq "/SampleService/RubyOData.svc/Products(3)" + end + end + + describe "sample service" do + before(:each) do + # Required for the build_classes method + stub_request(:get, /http:\/\/test\.com\/test\.svc\/\$metadata(?:\?.+)?/). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/sample_service/edmx_categories_products.xml", __FILE__)), :headers => {}) + + stub_request(:get, /http:\/\/test\.com\/test\.svc\/Products\(\d\)/). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/sample_service/result_single_product.xml", __FILE__)), :headers => {}) + + stub_request(:get, /http:\/\/test\.com\/test\.svc\/Products\(\d{2,}\)/). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/sample_service/result_single_product_not_found.xml", __FILE__)), :headers => {}) + + stub_request(:get, "http://test.com/test.svc/Products(1)/Category"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/sample_service/result_single_category.xml", __FILE__)), :headers => {}) + + stub_request(:get, "http://test.com/test.svc/Categories(1)"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/sample_service/result_single_category.xml", __FILE__)), :headers => {}) + + stub_request(:get, "http://test.com/test.svc/Categories(1)/Products"). + with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}). + to_return(:status => 200, :body => File.new(File.expand_path("../fixtures/sample_service/result_multiple_category_products.xml", __FILE__)), :headers => {}) + + stub_request(:post, "http://test.com/test.svc/Categories(1)/$links/Products").to_return(:status => 204) + stub_request(:post, "http://test.com/test.svc/$batch").to_return(:status => 202) + end + + describe "lazy loading" do + after(:each) do + Object.send(:remove_const, 'Product') if Object.const_defined? 'Product' + Object.send(:remove_const, 'Category') if Object.const_defined? 'Category' + end + + it "should have a load property method" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.should respond_to(:load_property) + end + + it "should throw an exception if the object isn't tracked" do + svc = OData::Service.new "http://test.com/test.svc/" + new_object = Product.new + lambda { svc.load_property(new_object, "Category") }.should raise_error(ArgumentError, "You cannot load a property on an entity that isn't tracked") + end + + it "should throw an exception if there isn't a method matching the navigation property passed in" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Products(1) + product = svc.execute.first + lambda { svc.load_property(product, "NoMatchingMethod") }.should raise_error(ArgumentError, "'NoMatchingMethod' is not a valid navigation property") + end + + it "should throw an exception if the method passed in is a standard property (non-navigation)" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Products(1) + product = svc.execute.first + lambda { svc.load_property(product, "Name") }.should raise_error(ArgumentError, "'Name' is not a valid navigation property") + end + + it "should fill a single navigation property" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Products(1) + product = svc.execute.first + svc.load_property(product, "Category") + product.Category.should_not be_nil + product.Category.Id.should eq 1 + product.Category.Name.should eq 'Category 1' + end + + it "should fill a collection navigation property" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1) + category = svc.execute.first + svc.load_property(category, "Products") + category.Products.first.should be_a Product + category.Products[0].Id.should eq 1 + category.Products[1].Id.should eq 2 + end + end + + describe "find, create, add, update, and delete" do + after(:each) do + Object.send(:remove_const, 'Product') if Object.const_defined? 'Product' + Object.send(:remove_const, 'Category') if Object.const_defined? 'Category' + end + + it "should implement an AddTo method for collection" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.should respond_to :AddToCategories + svc.should respond_to :AddToProducts + end + + it "should create objects with an initialize method that can build the object from a hash" do + svc = OData::Service.new "http://test.com/test.svc/" + product = Product.new 'Id' => 1000, 'Name' => 'New Product' + product.Id.should eq 1000 + product.Name.should eq 'New Product' + end + + it "should create objects that rejects keys that don't have corresponding methods" do + svc = OData::Service.new "http://test.com/test.svc/" + lambda { Product.new 'NotAProperty' => true }.should raise_error NoMethodError + end + + it "should create objects that expose a properties class method that lists the properties for the object" do + svc = OData::Service.new "http://test.com/test.svc/" + Product.properties.should include 'Id' + Product.properties.should include 'Name' + Product.properties.should include 'Category' + end + + it "should have full metadata for a property returned from the properties method" do + svc = OData::Service.new "http://test.com/test.svc/" + Product.properties['Category'].should be_a PropertyMetadata + Product.properties['Category'].nav_prop.should be_true + end + + it "should create objects that expose an id property" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Products(1) + product = svc.execute.first + product.should respond_to :id + end + + it "should extract the id from the metadata" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Products(1) + product = svc.execute.first + product.id.should eq 1 + end + + describe "Class.first method" do + it "should exist on the create server objects" do + svc = OData::Service.new "http://test.com/test.svc/" + Product.should respond_to :first + end + it "should return nil if an id isn't passed in" do + svc = OData::Service.new "http://test.com/test.svc/" + product = Product.first(nil) + product.should be_nil + end + it "should return nil if an id isn't found" do + svc = OData::Service.new "http://test.com/test.svc/" + product = Product.first(1234567890) + product.should be_nil + end + it "should return a product if an id is found" do + svc = OData::Service.new "http://test.com/test.svc/" + product = Product.first(1) + product.should_not be_nil + end + end + end + + describe "namespaces" do + after(:each) do + VisoftInc::Sample::Models.send(:remove_const, 'Product') if VisoftInc::Sample::Models.const_defined? 'Product' + VisoftInc::Sample::Models.send(:remove_const, 'Category') if VisoftInc::Sample::Models.const_defined? 'Category' + + VisoftInc::Sample.send(:remove_const, 'Models') if VisoftInc::Sample.const_defined? 'Models' + VisoftInc.send(:remove_const, 'Sample') if VisoftInc.const_defined? 'Sample' + Object.send(:remove_const, 'VisoftInc') if Object.const_defined? 'VisoftInc' + end + + it "should create models in the specified namespace if the option is set (using a .NET style namespace with dots)" do + svc = OData::Service.new "http://test.com/test.svc/", { :namespace => 'VisoftInc.Sample.Models' } + defined?(VisoftInc::Sample::Models::Product).nil?.should be_false, 'VisoftInc::Sample::Models::Product was expected to be defined, but was not' + defined?(VisoftInc::Sample::Models::Category).nil?.should be_false, 'VisoftInc::Sample::Models::Category was expected to be defined, but was not' + end + + it "should create models in the specified namespace if the option is set (using Ruby style namespaces with double colons)" do + svc = OData::Service.new "http://test.com/test.svc/", { :namespace => 'VisoftInc::Sample::Models' } + defined?(VisoftInc::Sample::Models::Product).nil?.should be_false, 'VisoftInc::Sample::Models::Product was expected to be defined, but was not' + defined?(VisoftInc::Sample::Models::Category).nil?.should be_false, 'VisoftInc::Sample::Models::Category was expected to be defined, but was not' + end + + it "should fill object defined in a namespace" do + svc = OData::Service.new "http://test.com/test.svc/", { :namespace => 'VisoftInc::Sample::Models' } + svc.Categories(1) + categories = svc.execute + categories.should_not be_nil + category = categories.first + category.Id.should eq 1 + category.Name.should eq 'Category 1' + end + + it "should fill the class_metadata hash" do + svc = OData::Service.new "http://test.com/test.svc/", { :namespace => 'VisoftInc::Sample::Models' } + svc.class_metadata.should_not be_empty + end + + it "should add a key (based on the name) for each property class_metadata hash" do + svc = OData::Service.new "http://test.com/test.svc/", { :namespace => 'VisoftInc::Sample::Models' } + svc.class_metadata['VisoftInc::Sample::Models::Product'].should have_key 'Id' + svc.class_metadata['VisoftInc::Sample::Models::Product'].should have_key 'Name' + svc.class_metadata['VisoftInc::Sample::Models::Product'].should have_key 'Description' + end + + it "should lazy load objects defined in a namespace" do + svc = OData::Service.new "http://test.com/test.svc/", { :namespace => 'VisoftInc::Sample::Models' } + svc.Categories(1) + category = svc.execute.first + svc.load_property category, 'Products' + category.Products.should_not be_nil + category.Products.first.Id.should eq 1 + category.Products.first.Name.should eq 'Widget 1' + end + end + + describe "add_link method" do + it "should exist as a method on the service" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.should respond_to(:add_link) + end + + it "shouldn't be allowed if a parent isn't tracked" do + svc = OData::Service.new "http://test.com/test.svc/" + category = Category.new :Name => 'New Category' + property = nil # Not needed for this test + product = nil # Not needed for this test + lambda { svc.add_link(category, property, product) }.should raise_error(ArgumentError, "You cannot add a link on an entity that isn't tracked (Category)") + end + + it "shouldn't be allowed if a property isn't found on the parent" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1) + category = svc.execute.first + property = 'NotAProperty' + product = nil # Not needed for this test + lambda { svc.add_link(category, property, product) }.should raise_error(ArgumentError, "'NotAProperty' is not a valid navigation property for Category") + end + + it "shouldn't be allowed if a property isn't a navigation property on the parent" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1) + category = svc.execute.first + property = 'Name' + product = nil # Not needed for this test + lambda { svc.add_link(category, property, product) }.should raise_error(ArgumentError, "'Name' is not a valid navigation property for Category") + end + + it "shouldn't be allowed if a child isn't tracked" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1) + category = svc.execute.first + property = 'Products' + product = Product.new :Name => 'Widget 1' + lambda { svc.add_link(category, property, product) }.should raise_error(ArgumentError, "You cannot add a link on a child entity that isn't tracked (Product)") + end + + it "should perform a post against the correct URL with the correct body on a single_save" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1) + category = svc.execute.first + svc.Products(1) + product = svc.execute.first + property = 'Products' + svc.add_link(category, property, product) + svc.save_changes + a_request(:post, "http://test.com/test.svc/Categories(1)/$links/Products"). + with(:body => { "uri" => "http://test.com/test.svc/Products(1)" }, + :headers => {'Content-Type' => 'application/json'}).should have_been_made + end + + it "should add the child to the parent's navigation property on a single_save" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1) + category = svc.execute.first + svc.Products(1) + product = svc.execute.first + property = 'Products' + svc.add_link(category, property, product) + svc.save_changes + category.Products.should include product + end + + it "should add the parent to the child's navigation property on a single_save" do + svc = OData::Service.new "http://test.com/test.svc/" + svc.Categories(1) + category = svc.execute.first + svc.Products(1) + product = svc.execute.first + property = 'Products' + svc.add_link(category, property, product) + svc.save_changes + product.Category.should eq category + end + + describe "batch_save" do + before(:each) do + @svc = OData::Service.new "http://test.com/test.svc/" + @category = Category.first(1) + @product = Product.first(1) + new_category = Category.new + @svc.AddToCategories(new_category) + @svc.add_link(@category, 'Products', @product) + @svc.save_changes + end + + it "should perform a post with the correct URL and body on a batch_save" do + WebMock.should have_requested(:post, "http://test.com/test.svc/$batch").with { |request| + request.body.include? "POST http://test.com/test.svc/Categories(1)/$links/Products HTTP/1.1" + request.body.include? '{"uri":"http://test.com/test.svc/Products(1)"}' + } + end + context "child is a part of the parent's collection" do + subject { @category.Products } + it { should include @product } + end + context "parent object should be filled in on the child" do + subject { @product.Category } + it { should eq @category } + end + end + end + end + end + describe_private OData::Service do describe "parse value" do before(:each) do # Required for the build_classes method stub_request(:get, "http://test.com/test.svc/$metadata"). \ No newline at end of file