.*)\Z")
+
+ def __init__(self, scopes):
+ if isinstance(scopes, basestring_type()):
+ scopes = scopes.split(self.SCOPE_DELIMITER)
+
+ self.__store_scopes(scopes)
+
+ def covers(self, api_access):
+ return api_access._compressed_scopes <= self._expanded_scopes
+
+ def __str__(self):
+ return self.SCOPE_DELIMITER.join(self._compressed_scopes)
+
+ def __iter__(self):
+ return iter(self._compressed_scopes)
+
+ def __eq__(self, other):
+ return type(self) == type(other) and self._compressed_scopes == other._compressed_scopes
+
+ def __store_scopes(self, scopes):
+ sanitized_scopes = frozenset(filter(None, [scope.strip() for scope in scopes]))
+ self.__validate_scopes(sanitized_scopes)
+ implied_scopes = frozenset(self.__implied_scope(scope) for scope in sanitized_scopes)
+ self._compressed_scopes = sanitized_scopes - implied_scopes
+ self._expanded_scopes = sanitized_scopes.union(implied_scopes)
+
+ def __validate_scopes(self, scopes):
+ for scope in scopes:
+ if not self.SCOPE_RE.match(scope):
+ error_message = "'{s}' is not a valid access scope".format(s=scope)
+ raise ApiAccessError(error_message)
+
+ def __implied_scope(self, scope):
+ match = self.IMPLIED_SCOPE_RE.match(scope)
+ if match:
+ return "{unauthenticated}read_{resource}".format(
+ unauthenticated=match.group("unauthenticated") or "",
+ resource=match.group("resource"),
+ )
diff --git a/shopify/api_version.py b/shopify/api_version.py
new file mode 100644
index 00000000..32276668
--- /dev/null
+++ b/shopify/api_version.py
@@ -0,0 +1,95 @@
+import re
+
+
+class InvalidVersionError(Exception):
+ pass
+
+
+class VersionNotFoundError(Exception):
+ pass
+
+
+class ApiVersion(object):
+ versions = {}
+
+ @classmethod
+ def coerce_to_version(cls, version):
+ try:
+ return cls.versions[version]
+ except KeyError:
+ # Dynamically create a new Release object if version string is not found
+ if Release.FORMAT.match(version):
+ return Release(version)
+ raise VersionNotFoundError
+
+ @classmethod
+ def define_version(cls, version):
+ cls.versions[version.name] = version
+ return version
+
+ @classmethod
+ def define_known_versions(cls):
+ cls.define_version(Unstable())
+ cls.define_version(Release("2021-10"))
+ cls.define_version(Release("2022-01"))
+ cls.define_version(Release("2022-04"))
+ cls.define_version(Release("2022-07"))
+ cls.define_version(Release("2022-10"))
+ cls.define_version(Release("2023-01"))
+ cls.define_version(Release("2023-04"))
+ cls.define_version(Release("2023-07"))
+ cls.define_version(Release("2023-10"))
+ cls.define_version(Release("2024-01"))
+ cls.define_version(Release("2024-04"))
+ cls.define_version(Release("2024-07"))
+ cls.define_version(Release("2024-10"))
+
+ @classmethod
+ def clear_defined_versions(cls):
+ cls.versions = {}
+
+ @property
+ def numeric_version(self):
+ return self._numeric_version
+
+ @property
+ def name(self):
+ return self._name
+
+ def api_path(self, site):
+ return site + self._path
+
+ def __eq__(self, other):
+ if not isinstance(other, type(self)):
+ return False
+ return self.numeric_version == int(other.numeric_version)
+
+
+class Release(ApiVersion):
+ FORMAT = re.compile(r"^\d{4}-\d{2}$")
+ API_PREFIX = "/admin/api"
+
+ def __init__(self, version_number):
+ if not self.FORMAT.match(version_number):
+ raise InvalidVersionError
+ self._name = version_number
+ self._numeric_version = int(version_number.replace("-", ""))
+ self._path = "%s/%s" % (self.API_PREFIX, version_number)
+
+ @property
+ def stable(self):
+ return True
+
+
+class Unstable(ApiVersion):
+ def __init__(self):
+ self._name = "unstable"
+ self._numeric_version = 9000000
+ self._path = "/admin/api/unstable"
+
+ @property
+ def stable(self):
+ return False
+
+
+ApiVersion.define_known_versions()
diff --git a/shopify/base.py b/shopify/base.py
new file mode 100644
index 00000000..449e288b
--- /dev/null
+++ b/shopify/base.py
@@ -0,0 +1,199 @@
+import pyactiveresource.connection
+from pyactiveresource.activeresource import ActiveResource, ResourceMeta, formats
+import shopify.yamlobjects
+import shopify.mixins as mixins
+import shopify
+import threading
+import sys
+from six.moves import urllib
+import six
+
+from shopify.collection import PaginatedCollection
+from pyactiveresource.collection import Collection
+
+# Store the response from the last request in the connection object
+
+
+class ShopifyConnection(pyactiveresource.connection.Connection):
+ response = None
+
+ def _open(self, *args, **kwargs):
+ self.response = None
+ try:
+ self.response = super(ShopifyConnection, self)._open(*args, **kwargs)
+ except pyactiveresource.connection.ConnectionError as err:
+ self.response = err.response
+ raise
+ return self.response
+
+
+# Inherit from pyactiveresource's metaclass in order to use ShopifyConnection
+
+
+class ShopifyResourceMeta(ResourceMeta):
+ @property
+ def connection(cls):
+ """HTTP connection for the current thread"""
+ local = cls._threadlocal
+ if not getattr(local, "connection", None):
+ # Make sure these variables are no longer affected by other threads.
+ local.user = cls.user
+ local.password = cls.password
+ local.site = cls.site
+ local.timeout = cls.timeout
+ local.headers = cls.headers
+ local.format = cls.format
+ local.version = cls.version
+ local.url = cls.url
+ if cls.site is None:
+ raise ValueError("No shopify session is active")
+ local.connection = ShopifyConnection(cls.site, cls.user, cls.password, cls.timeout, cls.format)
+ return local.connection
+
+ def get_user(cls):
+ return getattr(cls._threadlocal, "user", ShopifyResource._user)
+
+ def set_user(cls, value):
+ cls._threadlocal.connection = None
+ ShopifyResource._user = cls._threadlocal.user = value
+
+ user = property(get_user, set_user, None, "The username for HTTP Basic Auth.")
+
+ def get_password(cls):
+ return getattr(cls._threadlocal, "password", ShopifyResource._password)
+
+ def set_password(cls, value):
+ cls._threadlocal.connection = None
+ ShopifyResource._password = cls._threadlocal.password = value
+
+ password = property(get_password, set_password, None, "The password for HTTP Basic Auth.")
+
+ def get_site(cls):
+ return getattr(cls._threadlocal, "site", ShopifyResource._site)
+
+ def set_site(cls, value):
+ cls._threadlocal.connection = None
+ ShopifyResource._site = cls._threadlocal.site = value
+ if value is not None:
+ parts = urllib.parse.urlparse(value)
+ host = parts.hostname
+ if parts.port:
+ host += ":" + str(parts.port)
+ new_site = urllib.parse.urlunparse((parts.scheme, host, parts.path, "", "", ""))
+ ShopifyResource._site = cls._threadlocal.site = new_site
+ if parts.username:
+ cls.user = urllib.parse.unquote(parts.username)
+ if parts.password:
+ cls.password = urllib.parse.unquote(parts.password)
+
+ site = property(get_site, set_site, None, "The base REST site to connect to.")
+
+ def get_timeout(cls):
+ return getattr(cls._threadlocal, "timeout", ShopifyResource._timeout)
+
+ def set_timeout(cls, value):
+ cls._threadlocal.connection = None
+ ShopifyResource._timeout = cls._threadlocal.timeout = value
+
+ timeout = property(get_timeout, set_timeout, None, "Socket timeout for HTTP requests")
+
+ def get_headers(cls):
+ if not hasattr(cls._threadlocal, "headers"):
+ cls._threadlocal.headers = ShopifyResource._headers.copy()
+ return cls._threadlocal.headers
+
+ def set_headers(cls, value):
+ cls._threadlocal.headers = value
+
+ headers = property(get_headers, set_headers, None, "The headers sent with HTTP requests")
+
+ def get_format(cls):
+ return getattr(cls._threadlocal, "format", ShopifyResource._format)
+
+ def set_format(cls, value):
+ cls._threadlocal.connection = None
+ ShopifyResource._format = cls._threadlocal.format = value
+
+ format = property(get_format, set_format, None, "Encoding used for request and responses")
+
+ def get_prefix_source(cls):
+ """Return the prefix source, by default derived from site."""
+ try:
+ return cls.override_prefix()
+ except AttributeError:
+ if hasattr(cls, "_prefix_source"):
+ return cls.site + cls._prefix_source
+ else:
+ return cls.site
+
+ def set_prefix_source(cls, value):
+ """Set the prefix source, which will be rendered into the prefix."""
+ cls._prefix_source = value
+
+ prefix_source = property(get_prefix_source, set_prefix_source, None, "prefix for lookups for this type of object.")
+
+ def get_version(cls):
+ if hasattr(cls._threadlocal, "version") or ShopifyResource._version:
+ return getattr(cls._threadlocal, "version", ShopifyResource._version)
+ elif ShopifyResource._site is not None:
+ return ShopifyResource._site.split("/")[-1]
+
+ def set_version(cls, value):
+ ShopifyResource._version = cls._threadlocal.version = value
+
+ version = property(get_version, set_version, None, "Shopify Api Version")
+
+ def get_url(cls):
+ return getattr(cls._threadlocal, "url", ShopifyResource._url)
+
+ def set_url(cls, value):
+ ShopifyResource._url = cls._threadlocal.url = value
+
+ url = property(get_url, set_url, None, "Base URL including protocol and shopify domain")
+
+
+@six.add_metaclass(ShopifyResourceMeta)
+class ShopifyResource(ActiveResource, mixins.Countable):
+ _format = formats.JSONFormat
+ _threadlocal = threading.local()
+ _headers = {"User-Agent": "ShopifyPythonAPI/%s Python/%s" % (shopify.VERSION, sys.version.split(" ", 1)[0])}
+ _version = None
+ _url = None
+
+ def __init__(self, attributes=None, prefix_options=None):
+ if attributes is not None and prefix_options is None:
+ prefix_options, attributes = self.__class__._split_options(attributes)
+ return super(ShopifyResource, self).__init__(attributes, prefix_options)
+
+ def is_new(self):
+ return not self.id
+
+ def _load_attributes_from_response(self, response):
+ if response.body.strip():
+ self._update(self.__class__.format.decode(response.body))
+
+ @classmethod
+ def activate_session(cls, session):
+ cls.site = session.site
+ cls.url = session.url
+ cls.user = None
+ cls.password = None
+ cls.version = session.api_version.name
+ cls.headers["X-Shopify-Access-Token"] = session.token
+
+ @classmethod
+ def clear_session(cls):
+ cls.site = None
+ cls.url = None
+ cls.user = None
+ cls.password = None
+ cls.version = None
+ cls.headers.pop("X-Shopify-Access-Token", None)
+
+ @classmethod
+ def find(cls, id_=None, from_=None, **kwargs):
+ """Checks the resulting collection for pagination metadata."""
+ collection = super(ShopifyResource, cls).find(id_=id_, from_=from_, **kwargs)
+ if isinstance(collection, Collection) and "headers" in collection.metadata:
+ return PaginatedCollection(collection, metadata={"resource_class": cls}, **kwargs)
+ return collection
diff --git a/shopify/collection.py b/shopify/collection.py
new file mode 100644
index 00000000..62728eb9
--- /dev/null
+++ b/shopify/collection.py
@@ -0,0 +1,156 @@
+from pyactiveresource.collection import Collection
+
+
+class PaginatedCollection(Collection):
+ """
+ A subclass of Collection which allows cycling through pages of
+ data through cursor-based pagination.
+
+ :next_page_url contains a url for fetching the next page
+ :previous_page_url contains a url for fetching the previous page
+
+ You can use next_page_url and previous_page_url to fetch the next page
+ of data by calling Resource.find(from_=page.next_page_url)
+ """
+
+ def __init__(self, *args, **kwargs):
+ """If given a Collection object as an argument, inherit its metadata."""
+
+ metadata = kwargs.pop("metadata", None)
+ obj = args[0]
+ if isinstance(obj, Collection):
+ if metadata:
+ metadata.update(obj.metadata)
+ else:
+ metadata = obj.metadata
+ super(PaginatedCollection, self).__init__(obj, metadata=metadata)
+ else:
+ super(PaginatedCollection, self).__init__(metadata=metadata or {}, *args, **kwargs)
+
+ if not ("resource_class" in self.metadata):
+ raise AttributeError('Cursor-based pagination requires a "resource_class" attribute in the metadata.')
+
+ self.metadata["pagination"] = self.__parse_pagination()
+ self.next_page_url = self.metadata["pagination"].get("next", None)
+ self.previous_page_url = self.metadata["pagination"].get("previous", None)
+
+ self._next = None
+ self._previous = None
+ self._current_iter = None
+ self._no_iter_next = kwargs.pop("no_iter_next", True)
+
+ def __parse_pagination(self):
+ if "headers" not in self.metadata:
+ return {}
+
+ values = self.metadata["headers"].get("Link", self.metadata["headers"].get("link", None))
+ if values is None:
+ return {}
+
+ result = {}
+ for value in values.split(", "):
+ link, rel = value.split("; ")
+ result[rel.split('"')[1]] = link[1:-1]
+ return result
+
+ def has_previous_page(self):
+ """Returns true if the current page has any previous pages before it."""
+ return bool(self.previous_page_url)
+
+ def has_next_page(self):
+ """Returns true if the current page has any pages beyond the current position."""
+ return bool(self.next_page_url)
+
+ def previous_page(self, no_cache=False):
+ """Returns the previous page of items.
+
+ Args:
+ no_cache: If true the page will not be cached.
+ Returns:
+ A PaginatedCollection object with the new data set.
+ """
+ if self._previous:
+ return self._previous
+ elif not self.has_previous_page():
+ raise IndexError("No previous page")
+ return self.__fetch_page(self.previous_page_url, no_cache)
+
+ def next_page(self, no_cache=False):
+ """Returns the next page of items.
+
+ Args:
+ no_cache: If true the page will not be cached.
+ Returns:
+ A PaginatedCollection object with the new data set.
+ """
+ if self._next:
+ return self._next
+ elif not self.has_next_page():
+ raise IndexError("No next page")
+ return self.__fetch_page(self.next_page_url, no_cache)
+
+ def __fetch_page(self, url, no_cache=False):
+ next = self.metadata["resource_class"].find(from_=url)
+ if not no_cache:
+ self._next = next
+ self._next._previous = self
+ next._no_iter_next = self._no_iter_next
+ return next
+
+ def __iter__(self):
+ """Iterates through all items, also fetching other pages."""
+ for item in super(PaginatedCollection, self).__iter__():
+ yield item
+
+ if self._no_iter_next:
+ return
+
+ try:
+ if not self._current_iter:
+ self._current_iter = self
+ self._current_iter = self.next_page()
+
+ for item in self._current_iter:
+ yield item
+ except IndexError:
+ return
+
+ def __len__(self):
+ """If fetched count all the pages."""
+
+ if self._next:
+ count = len(self._next)
+ else:
+ count = 0
+ return count + super(PaginatedCollection, self).__len__()
+
+
+class PaginatedIterator(object):
+ """
+ This class implements an iterator over paginated collections which aims to
+ be more memory-efficient by not keeping more than one page in memory at a
+ time.
+
+ >>> from shopify import Product, PaginatedIterator
+ >>> for page in PaginatedIterator(Product.find()):
+ ... for item in page:
+ ... do_something(item)
+ ...
+ # every page and the page items are iterated
+ """
+
+ def __init__(self, collection):
+ if not isinstance(collection, PaginatedCollection):
+ raise TypeError("PaginatedIterator expects a PaginatedCollection instance")
+ self.collection = collection
+ self.collection._no_iter_next = True
+
+ def __iter__(self):
+ """Iterate over pages, returning one page at a time."""
+ current_page = self.collection
+ while True:
+ yield current_page
+ try:
+ current_page = current_page.next_page(no_cache=True)
+ except IndexError:
+ return
diff --git a/shopify/limits.py b/shopify/limits.py
new file mode 100644
index 00000000..0246c793
--- /dev/null
+++ b/shopify/limits.py
@@ -0,0 +1,62 @@
+import shopify
+
+
+class Limits(object):
+ """
+ API Calls Limit
+ https://help.shopify.com/en/api/getting-started/api-call-limit
+
+ Conversion of lib/shopify_api/limits.rb
+ """
+
+ # num_requests_executed/max_requests
+ # Eg: 1/40
+ CREDIT_LIMIT_HEADER_PARAM = "X-Shopify-Shop-Api-Call-Limit"
+
+ @classmethod
+ def response(cls):
+ if not shopify.Shop.connection.response:
+ shopify.Shop.current()
+ return shopify.Shop.connection.response
+
+ @classmethod
+ def api_credit_limit_param(cls):
+ response = cls.response()
+ _safe_header = getattr(response, "headers", "")
+
+ if not _safe_header:
+ raise Exception("No shopify headers found")
+
+ if cls.CREDIT_LIMIT_HEADER_PARAM in response.headers:
+ credits = response.headers[cls.CREDIT_LIMIT_HEADER_PARAM]
+ return credits.split("/")
+ else:
+ raise Exception("No valid api call header found")
+
+ @classmethod
+ def credit_left(cls):
+ """
+ How many more API calls can I make?
+ """
+ return int(cls.credit_limit() - cls.credit_used())
+
+ @classmethod
+ def credit_maxed(cls):
+ """
+ Have I reached my API call limit?
+ """
+ return bool(cls.credit_left() <= 0)
+
+ @classmethod
+ def credit_limit(cls):
+ """
+ How many total API calls can I make?
+ """
+ return int(cls.api_credit_limit_param()[1])
+
+ @classmethod
+ def credit_used(cls):
+ """
+ How many API calls have I made?
+ """
+ return int(cls.api_credit_limit_param()[0])
diff --git a/shopify/mixins.py b/shopify/mixins.py
new file mode 100644
index 00000000..5a13ca3a
--- /dev/null
+++ b/shopify/mixins.py
@@ -0,0 +1,34 @@
+import shopify.resources
+
+
+class Countable(object):
+ @classmethod
+ def count(cls, _options=None, **kwargs):
+ if _options is None:
+ _options = kwargs
+ return int(cls.get("count", **_options))
+
+
+class Metafields(object):
+ def metafields(self, _options=None, **kwargs):
+ if _options is None:
+ _options = kwargs
+ return shopify.resources.Metafield.find(resource=self.__class__.plural, resource_id=self.id, **_options)
+
+ def metafields_count(self, _options=None, **kwargs):
+ if _options is None:
+ _options = kwargs
+ return int(self.get("metafields/count", **_options))
+
+ def add_metafield(self, metafield):
+ if self.is_new():
+ raise ValueError("You can only add metafields to a resource that has been saved")
+
+ metafield._prefix_options = {"resource": self.__class__.plural, "resource_id": self.id}
+ metafield.save()
+ return metafield
+
+
+class Events(object):
+ def events(self):
+ return shopify.resources.Event.find(resource=self.__class__.plural, resource_id=self.id)
diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py
new file mode 100644
index 00000000..0d420b38
--- /dev/null
+++ b/shopify/resources/__init__.py
@@ -0,0 +1,82 @@
+from .shop import Shop
+from .product import Product
+from .cart import Cart
+from .checkout import Checkout
+from .custom_collection import CustomCollection
+from .collect import Collect
+from .shipping_address import ShippingAddress
+from .billing_address import BillingAddress
+from .line_item import LineItem
+from .shipping_line import ShippingLine
+from .note_attribute import NoteAttribute
+from .address import Address
+from .option import Option
+from .payment_details import PaymentDetails
+from .receipt import Receipt
+from .rule import Rule
+from .tax_line import TaxLine
+from .script_tag import ScriptTag
+from .application_charge import ApplicationCharge
+from .application_credit import ApplicationCredit
+from .recurring_application_charge import RecurringApplicationCharge
+from .usage_charge import UsageCharge
+from .asset import Asset
+from .theme import Theme
+from .currency import Currency
+from .customer_saved_search import CustomerSavedSearch
+from .customer_group import CustomerGroup
+from .customer_invite import CustomerInvite
+from .customer import Customer
+from .event import Event
+from .webhook import Webhook
+from .redirect import Redirect
+from .province import Province
+from .comment import Comment
+from .metafield import Metafield
+from .article import Article
+from .blog import Blog
+from .page import Page
+from .country import Country
+from .refund import Refund
+from .fulfillment import Fulfillment, FulfillmentOrders, FulfillmentV2
+from .fulfillment_event import FulfillmentEvent
+from .fulfillment_service import FulfillmentService
+from .carrier_service import CarrierService
+from .transaction import Transaction
+from .tender_transaction import TenderTransaction
+from .image import Image
+from .variant import Variant
+from .order import Order
+from .balance import Balance
+from .disputes import Disputes
+from .payouts import Payouts
+from .transactions import Transactions
+from .order_risk import OrderRisk
+from .policy import Policy
+from .smart_collection import SmartCollection
+from .gift_card import GiftCard
+from .gift_card_adjustment import GiftCardAdjustment
+from .shipping_zone import ShippingZone
+from .location import Location
+from .draft_order import DraftOrder
+from .draft_order_invoice import DraftOrderInvoice
+from .report import Report
+from .price_rule import PriceRule
+from .discount_code import DiscountCode
+from .discount_code_creation import DiscountCodeCreation
+from .marketing_event import MarketingEvent
+from .collection_listing import CollectionListing
+from .product_listing import ProductListing
+from .resource_feedback import ResourceFeedback
+from .storefront_access_token import StorefrontAccessToken
+from .inventory_item import InventoryItem
+from .inventory_level import InventoryLevel
+from .access_scope import AccessScope
+from .user import User
+from .api_permission import ApiPermission
+from .publication import Publication
+from .collection_publication import CollectionPublication
+from .product_publication import ProductPublication
+from .graphql import GraphQL
+
+from ..base import ShopifyResource
diff --git a/shopify/resources/access_scope.py b/shopify/resources/access_scope.py
new file mode 100644
index 00000000..1ded9d37
--- /dev/null
+++ b/shopify/resources/access_scope.py
@@ -0,0 +1,7 @@
+from ..base import ShopifyResource
+
+
+class AccessScope(ShopifyResource):
+ @classmethod
+ def override_prefix(cls):
+ return "/admin/oauth"
diff --git a/shopify/resources/address.py b/shopify/resources/address.py
new file mode 100644
index 00000000..b5d10718
--- /dev/null
+++ b/shopify/resources/address.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Address(ShopifyResource):
+ pass
diff --git a/shopify/resources/api_permission.py b/shopify/resources/api_permission.py
new file mode 100644
index 00000000..1c936451
--- /dev/null
+++ b/shopify/resources/api_permission.py
@@ -0,0 +1,9 @@
+from ..base import ShopifyResource
+
+
+class ApiPermission(ShopifyResource):
+ @classmethod
+ def delete(cls):
+ cls.connection.delete(cls.site + "/api_permissions/current." + cls.format.extension, cls.headers)
+
+ destroy = delete
diff --git a/shopify/resources/application_charge.py b/shopify/resources/application_charge.py
new file mode 100644
index 00000000..6ed62f77
--- /dev/null
+++ b/shopify/resources/application_charge.py
@@ -0,0 +1,6 @@
+from ..base import ShopifyResource
+
+
+class ApplicationCharge(ShopifyResource):
+ def activate(self):
+ self._load_attributes_from_response(self.post("activate"))
diff --git a/shopify/resources/application_credit.py b/shopify/resources/application_credit.py
new file mode 100644
index 00000000..ecc12fa0
--- /dev/null
+++ b/shopify/resources/application_credit.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class ApplicationCredit(ShopifyResource):
+ pass
diff --git a/shopify/resources/article.py b/shopify/resources/article.py
new file mode 100644
index 00000000..2b061a3e
--- /dev/null
+++ b/shopify/resources/article.py
@@ -0,0 +1,26 @@
+from ..base import ShopifyResource
+from shopify import mixins
+from .comment import Comment
+
+
+class Article(ShopifyResource, mixins.Metafields, mixins.Events):
+ _prefix_source = "/blogs/$blog_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ blog_id = options.get("blog_id")
+ if blog_id:
+ return "%s/blogs/%s" % (cls.site, blog_id)
+ else:
+ return cls.site
+
+ def comments(self):
+ return Comment.find(article_id=self.id)
+
+ @classmethod
+ def authors(cls, **kwargs):
+ return cls.get("authors", **kwargs)
+
+ @classmethod
+ def tags(cls, **kwargs):
+ return cls.get("tags", **kwargs)
diff --git a/shopify/resources/asset.py b/shopify/resources/asset.py
new file mode 100644
index 00000000..d5156a5a
--- /dev/null
+++ b/shopify/resources/asset.py
@@ -0,0 +1,82 @@
+from ..base import ShopifyResource
+import base64
+
+
+class Asset(ShopifyResource):
+ _primary_key = "key"
+ _prefix_source = "/themes/$theme_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ theme_id = options.get("theme_id")
+ if theme_id:
+ return "%s/themes/%s" % (cls.site, theme_id)
+ else:
+ return cls.site
+
+ @classmethod
+ def _element_path(cls, id, prefix_options={}, query_options=None):
+ if query_options is None:
+ prefix_options, query_options = cls._split_options(prefix_options)
+ return "%s%s.%s%s" % (
+ cls._prefix(prefix_options) + "/",
+ cls.plural,
+ cls.format.extension,
+ cls._query_string(query_options),
+ )
+
+ @classmethod
+ def find(cls, key=None, **kwargs):
+ """
+ Find an asset by key
+ E.g.
+ shopify.Asset.find('layout/theme.liquid', theme_id=99)
+ """
+ if not key:
+ return super(Asset, cls).find(**kwargs)
+
+ params = {"asset[key]": key}
+ params.update(kwargs)
+ theme_id = params.get("theme_id")
+ path_prefix = "%s/themes/%s" % (cls.site, theme_id) if theme_id else cls.site
+
+ resource = cls.find_one("%s/assets.%s" % (path_prefix, cls.format.extension), **params)
+
+ if theme_id and resource:
+ resource._prefix_options["theme_id"] = theme_id
+ return resource
+
+ def __get_value(self):
+ data = self.attributes.get("value")
+ if data:
+ return data
+ data = self.attributes.get("attachment")
+ if data:
+ return base64.b64decode(data).decode()
+
+ def __set_value(self, data):
+ self.__wipe_value_attributes()
+ self.attributes["value"] = data
+
+ value = property(__get_value, __set_value, None, "The asset's value or attachment")
+
+ def attach(self, data):
+ self.attachment = base64.b64encode(data).decode()
+
+ def destroy(self):
+ options = {"asset[key]": self.key}
+ options.update(self._prefix_options)
+ return self.__class__.connection.delete(self._element_path(self.key, options), self.__class__.headers)
+
+ def is_new(self):
+ return False
+
+ def __setattr__(self, name, value):
+ if name in ("value", "attachment", "src", "source_key"):
+ self.__wipe_value_attributes()
+ return super(Asset, self).__setattr__(name, value)
+
+ def __wipe_value_attributes(self):
+ for attr in ("value", "attachment", "src", "source_key"):
+ if attr in self.attributes:
+ del self.attributes[attr]
diff --git a/shopify/resources/balance.py b/shopify/resources/balance.py
new file mode 100644
index 00000000..aefa87ab
--- /dev/null
+++ b/shopify/resources/balance.py
@@ -0,0 +1,7 @@
+from ..base import ShopifyResource
+from shopify import mixins
+
+
+class Balance(ShopifyResource, mixins.Metafields):
+ _prefix_source = "/shopify_payments/"
+ _singular = _plural = "balance"
diff --git a/shopify/resources/billing_address.py b/shopify/resources/billing_address.py
new file mode 100644
index 00000000..4bf328db
--- /dev/null
+++ b/shopify/resources/billing_address.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class BillingAddress(ShopifyResource):
+ pass
diff --git a/shopify/resources/blog.py b/shopify/resources/blog.py
new file mode 100644
index 00000000..e88b26b1
--- /dev/null
+++ b/shopify/resources/blog.py
@@ -0,0 +1,8 @@
+from ..base import ShopifyResource
+from shopify import mixins
+import shopify
+
+
+class Blog(ShopifyResource, mixins.Metafields, mixins.Events):
+ def articles(self):
+ return shopify.Article.find(blog_id=self.id)
diff --git a/shopify/resources/carrier_service.py b/shopify/resources/carrier_service.py
new file mode 100644
index 00000000..635e9d0a
--- /dev/null
+++ b/shopify/resources/carrier_service.py
@@ -0,0 +1,11 @@
+from ..base import ShopifyResource
+
+
+class CarrierService(ShopifyResource):
+ def __get_format(self):
+ return self.attributes.get("format")
+
+ def __set_format(self, data):
+ self.attributes["format"] = data
+
+ format = property(__get_format, __set_format, None, "Format attribute")
diff --git a/shopify/resources/cart.py b/shopify/resources/cart.py
new file mode 100644
index 00000000..da611d52
--- /dev/null
+++ b/shopify/resources/cart.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Cart(ShopifyResource):
+ pass
diff --git a/shopify/resources/checkout.py b/shopify/resources/checkout.py
new file mode 100644
index 00000000..f785529e
--- /dev/null
+++ b/shopify/resources/checkout.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Checkout(ShopifyResource):
+ pass
diff --git a/shopify/resources/collect.py b/shopify/resources/collect.py
new file mode 100644
index 00000000..f40b45ed
--- /dev/null
+++ b/shopify/resources/collect.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Collect(ShopifyResource):
+ pass
diff --git a/shopify/resources/collection_listing.py b/shopify/resources/collection_listing.py
new file mode 100644
index 00000000..00567489
--- /dev/null
+++ b/shopify/resources/collection_listing.py
@@ -0,0 +1,8 @@
+from ..base import ShopifyResource
+
+
+class CollectionListing(ShopifyResource):
+ _primary_key = "collection_id"
+
+ def product_ids(cls, **kwargs):
+ return cls.get("product_ids", **kwargs)
diff --git a/shopify/resources/collection_publication.py b/shopify/resources/collection_publication.py
new file mode 100644
index 00000000..6805d251
--- /dev/null
+++ b/shopify/resources/collection_publication.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class CollectionPublication(ShopifyResource):
+ _prefix_source = "/publications/$publication_id/"
diff --git a/shopify/resources/comment.py b/shopify/resources/comment.py
new file mode 100644
index 00000000..110afd61
--- /dev/null
+++ b/shopify/resources/comment.py
@@ -0,0 +1,18 @@
+from ..base import ShopifyResource
+
+
+class Comment(ShopifyResource):
+ def remove(self):
+ self._load_attributes_from_response(self.post("remove"))
+
+ def spam(self):
+ self._load_attributes_from_response(self.post("spam"))
+
+ def approve(self):
+ self._load_attributes_from_response(self.post("approve"))
+
+ def restore(self):
+ self._load_attributes_from_response(self.post("restore"))
+
+ def not_spam(self):
+ self._load_attributes_from_response(self.post("not_spam"))
diff --git a/shopify/resources/country.py b/shopify/resources/country.py
new file mode 100644
index 00000000..39839808
--- /dev/null
+++ b/shopify/resources/country.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Country(ShopifyResource):
+ pass
diff --git a/shopify/resources/currency.py b/shopify/resources/currency.py
new file mode 100644
index 00000000..6bf53b98
--- /dev/null
+++ b/shopify/resources/currency.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Currency(ShopifyResource):
+ pass
diff --git a/shopify/resources/custom_collection.py b/shopify/resources/custom_collection.py
new file mode 100644
index 00000000..85bcbc4a
--- /dev/null
+++ b/shopify/resources/custom_collection.py
@@ -0,0 +1,16 @@
+from ..base import ShopifyResource
+from shopify import mixins
+import shopify
+
+
+class CustomCollection(ShopifyResource, mixins.Metafields, mixins.Events):
+ def products(self):
+ return shopify.Product.find(collection_id=self.id)
+
+ def add_product(self, product):
+ return shopify.Collect.create({"collection_id": self.id, "product_id": product.id})
+
+ def remove_product(self, product):
+ collect = shopify.Collect.find_first(collection_id=self.id, product_id=product.id)
+ if collect:
+ collect.destroy()
diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py
new file mode 100644
index 00000000..ab989e84
--- /dev/null
+++ b/shopify/resources/customer.py
@@ -0,0 +1,29 @@
+from ..base import ShopifyResource
+from shopify import mixins
+from .customer_invite import CustomerInvite
+from .order import Order
+
+
+class Customer(ShopifyResource, mixins.Metafields):
+ @classmethod
+ def search(cls, **kwargs):
+ """
+ Search for customers matching supplied query
+
+ Args:
+ order: Field and direction to order results by (default: last_order_date DESC)
+ query: Text to search for customers
+ page: Page to show (default: 1)
+ limit: Amount of results (default: 50) (maximum: 250)
+ fields: comma-separated list of fields to include in the response
+ Returns:
+ A Collection of customers.
+ """
+ return cls._build_collection(cls.get("search", **kwargs))
+
+ def send_invite(self, customer_invite=CustomerInvite()):
+ resource = self.post("send_invite", customer_invite.encode())
+ return CustomerInvite(Customer.format.decode(resource.body))
+
+ def orders(self):
+ return Order.find(customer_id=self.id)
diff --git a/shopify/resources/customer_group.py b/shopify/resources/customer_group.py
new file mode 100644
index 00000000..0eb48de2
--- /dev/null
+++ b/shopify/resources/customer_group.py
@@ -0,0 +1,5 @@
+from .customer_saved_search import CustomerSavedSearch
+
+
+class CustomerGroup(CustomerSavedSearch):
+ pass
diff --git a/shopify/resources/customer_invite.py b/shopify/resources/customer_invite.py
new file mode 100644
index 00000000..cf4015b3
--- /dev/null
+++ b/shopify/resources/customer_invite.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class CustomerInvite(ShopifyResource):
+ pass
diff --git a/shopify/resources/customer_saved_search.py b/shopify/resources/customer_saved_search.py
new file mode 100644
index 00000000..dbe97251
--- /dev/null
+++ b/shopify/resources/customer_saved_search.py
@@ -0,0 +1,7 @@
+from ..base import ShopifyResource
+from .customer import Customer
+
+
+class CustomerSavedSearch(ShopifyResource):
+ def customers(cls, **kwargs):
+ return Customer._build_collection(cls.get("customers", **kwargs))
diff --git a/shopify/resources/discount_code.py b/shopify/resources/discount_code.py
new file mode 100644
index 00000000..e2559e3e
--- /dev/null
+++ b/shopify/resources/discount_code.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class DiscountCode(ShopifyResource):
+ _prefix_source = "/price_rules/$price_rule_id/"
diff --git a/shopify/resources/discount_code_creation.py b/shopify/resources/discount_code_creation.py
new file mode 100644
index 00000000..e72de283
--- /dev/null
+++ b/shopify/resources/discount_code_creation.py
@@ -0,0 +1,17 @@
+from ..base import ShopifyResource
+from .discount_code import DiscountCode
+
+
+class DiscountCodeCreation(ShopifyResource):
+ _prefix_source = "/price_rules/$price_rule_id/"
+
+ def discount_codes(self):
+ return DiscountCode.find(
+ from_="%s/price_rules/%s/batch/%s/discount_codes.%s"
+ % (
+ ShopifyResource.site,
+ self._prefix_options["price_rule_id"],
+ self.id,
+ DiscountCodeCreation.format.extension,
+ )
+ )
diff --git a/shopify/resources/disputes.py b/shopify/resources/disputes.py
new file mode 100644
index 00000000..f098fd0f
--- /dev/null
+++ b/shopify/resources/disputes.py
@@ -0,0 +1,6 @@
+from ..base import ShopifyResource
+from shopify import mixins
+
+
+class Disputes(ShopifyResource, mixins.Metafields):
+ _prefix_source = "/shopify_payments/"
diff --git a/shopify/resources/draft_order.py b/shopify/resources/draft_order.py
new file mode 100644
index 00000000..878cb7af
--- /dev/null
+++ b/shopify/resources/draft_order.py
@@ -0,0 +1,15 @@
+from ..base import ShopifyResource
+from shopify import mixins
+from .draft_order_invoice import DraftOrderInvoice
+
+
+class DraftOrder(ShopifyResource, mixins.Metafields):
+ def send_invoice(self, draft_order_invoice=DraftOrderInvoice()):
+ resource = self.post("send_invoice", draft_order_invoice.encode())
+ return DraftOrderInvoice(DraftOrder.format.decode(resource.body))
+
+ def complete(self, params={}):
+ if params.get("payment_pending", False):
+ self._load_attributes_from_response(self.put("complete", payment_pending="true"))
+ else:
+ self._load_attributes_from_response(self.put("complete"))
diff --git a/shopify/resources/draft_order_invoice.py b/shopify/resources/draft_order_invoice.py
new file mode 100644
index 00000000..c47b28e2
--- /dev/null
+++ b/shopify/resources/draft_order_invoice.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class DraftOrderInvoice(ShopifyResource):
+ pass
diff --git a/shopify/resources/event.py b/shopify/resources/event.py
new file mode 100644
index 00000000..f3268e13
--- /dev/null
+++ b/shopify/resources/event.py
@@ -0,0 +1,13 @@
+from ..base import ShopifyResource
+
+
+class Event(ShopifyResource):
+ _prefix_source = "/$resource/$resource_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ resource = options.get("resource")
+ if resource:
+ return "%s/%s/%s" % (cls.site, resource, options["resource_id"])
+ else:
+ return cls.site
diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py
new file mode 100644
index 00000000..fcf74863
--- /dev/null
+++ b/shopify/resources/fulfillment.py
@@ -0,0 +1,33 @@
+from ..base import ShopifyResource
+import json
+
+
+class Fulfillment(ShopifyResource):
+ _prefix_source = "/orders/$order_id/"
+
+ def cancel(self):
+ self._load_attributes_from_response(self.post("cancel"))
+
+ def complete(self):
+ self._load_attributes_from_response(self.post("complete"))
+
+ def open(self):
+ self._load_attributes_from_response(self.post("open"))
+
+ def update_tracking(self, tracking_info, notify_customer):
+ fulfill = FulfillmentV2()
+ fulfill.id = self.id
+ self._load_attributes_from_response(fulfill.update_tracking(tracking_info, notify_customer))
+
+
+class FulfillmentOrders(ShopifyResource):
+ _prefix_source = "/orders/$order_id/"
+
+
+class FulfillmentV2(ShopifyResource):
+ _singular = "fulfillment"
+ _plural = "fulfillments"
+
+ def update_tracking(self, tracking_info, notify_customer):
+ body = {"fulfillment": {"tracking_info": tracking_info, "notify_customer": notify_customer}}
+ return self.post("update_tracking", json.dumps(body).encode())
diff --git a/shopify/resources/fulfillment_event.py b/shopify/resources/fulfillment_event.py
new file mode 100644
index 00000000..fbd2ece7
--- /dev/null
+++ b/shopify/resources/fulfillment_event.py
@@ -0,0 +1,32 @@
+from ..base import ShopifyResource
+
+
+class FulfillmentEvent(ShopifyResource):
+ _prefix_source = "/orders/$order_id/fulfillments/$fulfillment_id/"
+ _singular = "event"
+ _plural = "events"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ order_id = options.get("order_id")
+ fulfillment_id = options.get("fulfillment_id")
+ event_id = options.get("event_id")
+
+ return "%s/orders/%s/fulfillments/%s" % (cls.site, order_id, fulfillment_id)
+
+ def save(self):
+ status = self.attributes["status"]
+ if status not in [
+ "label_printed",
+ "label_purchased",
+ "attempted_delivery",
+ "ready_for_pickup",
+ "picked_up",
+ "confirmed",
+ "in_transit",
+ "out_for_delivery",
+ "delivered",
+ "failure",
+ ]:
+ raise AttributeError("Invalid status")
+ return super(ShopifyResource, self).save()
diff --git a/shopify/resources/fulfillment_service.py b/shopify/resources/fulfillment_service.py
new file mode 100644
index 00000000..c2c465b7
--- /dev/null
+++ b/shopify/resources/fulfillment_service.py
@@ -0,0 +1,11 @@
+from ..base import ShopifyResource
+
+
+class FulfillmentService(ShopifyResource):
+ def __get_format(self):
+ return self.attributes.get("format")
+
+ def __set_format(self, data):
+ self.attributes["format"] = data
+
+ format = property(__get_format, __set_format, None, "Format attribute")
diff --git a/shopify/resources/gift_card.py b/shopify/resources/gift_card.py
new file mode 100644
index 00000000..c1918c68
--- /dev/null
+++ b/shopify/resources/gift_card.py
@@ -0,0 +1,30 @@
+from ..base import ShopifyResource
+from .gift_card_adjustment import GiftCardAdjustment
+
+
+class GiftCard(ShopifyResource):
+ def disable(self):
+ self._load_attributes_from_response(self.post("disable"))
+
+ @classmethod
+ def search(cls, **kwargs):
+ """
+ Search for gift cards matching supplied query
+
+ Args:
+ order: Field and direction to order results by (default: disabled_at DESC)
+ query: Text to search for gift cards
+ page: Page to show (default: 1)
+ limit: Amount of results (default: 50) (maximum: 250)
+ fields: comma-separated list of fields to include in the response
+ Returns:
+ An array of gift cards.
+ """
+ return cls._build_collection(cls.get("search", **kwargs))
+
+ def add_adjustment(self, adjustment):
+ """
+ Create a new Gift Card Adjustment
+ """
+ resource = self.post("adjustments", adjustment.encode())
+ return GiftCardAdjustment(GiftCard.format.decode(resource.body))
diff --git a/shopify/resources/gift_card_adjustment.py b/shopify/resources/gift_card_adjustment.py
new file mode 100644
index 00000000..2314cdb6
--- /dev/null
+++ b/shopify/resources/gift_card_adjustment.py
@@ -0,0 +1,7 @@
+from ..base import ShopifyResource
+
+
+class GiftCardAdjustment(ShopifyResource):
+ _prefix_source = "/admin/gift_cards/$gift_card_id/"
+ _plural = "adjustments"
+ _singular = "adjustment"
diff --git a/shopify/resources/graphql.py b/shopify/resources/graphql.py
new file mode 100644
index 00000000..33525ef1
--- /dev/null
+++ b/shopify/resources/graphql.py
@@ -0,0 +1,32 @@
+import shopify
+from ..base import ShopifyResource
+from six.moves import urllib
+import json
+
+
+class GraphQL:
+ def __init__(self):
+ self.endpoint = shopify.ShopifyResource.get_site() + "/graphql.json"
+ self.headers = shopify.ShopifyResource.get_headers()
+
+ def merge_headers(self, *headers):
+ merged_headers = {}
+ for header in headers:
+ merged_headers.update(header)
+ return merged_headers
+
+ def execute(self, query, variables=None, operation_name=None):
+ endpoint = self.endpoint
+ default_headers = {"Accept": "application/json", "Content-Type": "application/json"}
+ headers = self.merge_headers(default_headers, self.headers)
+ data = {"query": query, "variables": variables, "operationName": operation_name}
+
+ req = urllib.request.Request(self.endpoint, json.dumps(data).encode("utf-8"), headers)
+
+ try:
+ response = urllib.request.urlopen(req)
+ return response.read().decode("utf-8")
+ except urllib.error.HTTPError as e:
+ print((e.read()))
+ print("")
+ raise e
diff --git a/shopify/resources/image.py b/shopify/resources/image.py
new file mode 100644
index 00000000..1a4d13fb
--- /dev/null
+++ b/shopify/resources/image.py
@@ -0,0 +1,41 @@
+from ..base import ShopifyResource
+from ..resources import Metafield
+from six.moves import urllib
+import base64
+import re
+
+
+class Image(ShopifyResource):
+ _prefix_source = "/products/$product_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ product_id = options.get("product_id")
+ if product_id:
+ return "%s/products/%s" % (cls.site, product_id)
+ else:
+ return cls.site
+
+ def __getattr__(self, name):
+ if name in ["pico", "icon", "thumb", "small", "compact", "medium", "large", "grande", "original"]:
+ return re.sub(r"/(.*)\.(\w{2,4})", r"/\1_%s.\2" % (name), self.src)
+ else:
+ return super(Image, self).__getattr__(name)
+
+ def attach_image(self, data, filename=None):
+ self.attributes["attachment"] = base64.b64encode(data).decode()
+ if filename:
+ self.attributes["filename"] = filename
+
+ def metafields(self):
+ if self.is_new():
+ return []
+ query_params = {"metafield[owner_id]": self.id, "metafield[owner_resource]": "product_image"}
+ return Metafield.find(
+ from_="%s/metafields.json?%s" % (ShopifyResource.site, urllib.parse.urlencode(query_params))
+ )
+
+ def save(self):
+ if "product_id" not in self._prefix_options:
+ self._prefix_options["product_id"] = self.product_id
+ return super(ShopifyResource, self).save()
diff --git a/shopify/resources/inventory_item.py b/shopify/resources/inventory_item.py
new file mode 100644
index 00000000..8f3d39ce
--- /dev/null
+++ b/shopify/resources/inventory_item.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class InventoryItem(ShopifyResource):
+ pass
diff --git a/shopify/resources/inventory_level.py b/shopify/resources/inventory_level.py
new file mode 100644
index 00000000..5b7f4b0a
--- /dev/null
+++ b/shopify/resources/inventory_level.py
@@ -0,0 +1,58 @@
+from ..base import ShopifyResource
+import shopify
+import json
+
+
+class InventoryLevel(ShopifyResource):
+ def __repr__(self):
+ return "%s(inventory_item_id=%s, location_id=%s)" % (self._singular, self.inventory_item_id, self.location_id)
+
+ @classmethod
+ def _element_path(cls, prefix_options={}, query_options=None):
+ if query_options is None:
+ prefix_options, query_options = cls._split_options(prefix_options)
+
+ return "%s%s.%s%s" % (
+ cls._prefix(prefix_options) + "/",
+ cls.plural,
+ cls.format.extension,
+ cls._query_string(query_options),
+ )
+
+ @classmethod
+ def adjust(cls, location_id, inventory_item_id, available_adjustment):
+ body = {
+ "inventory_item_id": inventory_item_id,
+ "location_id": location_id,
+ "available_adjustment": available_adjustment,
+ }
+ resource = cls.post("adjust", body=json.dumps(body).encode())
+ return InventoryLevel(InventoryLevel.format.decode(resource.body))
+
+ @classmethod
+ def connect(cls, location_id, inventory_item_id, relocate_if_necessary=False, **kwargs):
+ body = {
+ "inventory_item_id": inventory_item_id,
+ "location_id": location_id,
+ "relocate_if_necessary": relocate_if_necessary,
+ }
+ resource = cls.post("connect", body=json.dumps(body).encode())
+ return InventoryLevel(InventoryLevel.format.decode(resource.body))
+
+ @classmethod
+ def set(cls, location_id, inventory_item_id, available, disconnect_if_necessary=False, **kwargs):
+ body = {
+ "inventory_item_id": inventory_item_id,
+ "location_id": location_id,
+ "available": available,
+ "disconnect_if_necessary": disconnect_if_necessary,
+ }
+ resource = cls.post("set", body=json.dumps(body).encode())
+ return InventoryLevel(InventoryLevel.format.decode(resource.body))
+
+ def is_new(self):
+ return False
+
+ def destroy(self):
+ options = {"inventory_item_id": self.inventory_item_id, "location_id": self.location_id}
+ return self.__class__.connection.delete(self._element_path(query_options=options), self.__class__.headers)
diff --git a/shopify/resources/line_item.py b/shopify/resources/line_item.py
new file mode 100644
index 00000000..c701c90f
--- /dev/null
+++ b/shopify/resources/line_item.py
@@ -0,0 +1,6 @@
+from ..base import ShopifyResource
+
+
+class LineItem(ShopifyResource):
+ class Property(ShopifyResource):
+ pass
diff --git a/shopify/resources/location.py b/shopify/resources/location.py
new file mode 100644
index 00000000..51e7ecdd
--- /dev/null
+++ b/shopify/resources/location.py
@@ -0,0 +1,9 @@
+from ..base import ShopifyResource
+from .inventory_level import InventoryLevel
+
+
+class Location(ShopifyResource):
+ def inventory_levels(self, **kwargs):
+ return InventoryLevel.find(
+ from_="%s/locations/%s/inventory_levels.json" % (ShopifyResource.site, self.id), **kwargs
+ )
diff --git a/shopify/resources/marketing_event.py b/shopify/resources/marketing_event.py
new file mode 100644
index 00000000..6b629449
--- /dev/null
+++ b/shopify/resources/marketing_event.py
@@ -0,0 +1,8 @@
+import json
+from ..base import ShopifyResource
+
+
+class MarketingEvent(ShopifyResource):
+ def add_engagements(self, engagements):
+ engagements_json = json.dumps({"engagements": engagements})
+ return self.post("engagements", engagements_json.encode())
diff --git a/shopify/resources/metafield.py b/shopify/resources/metafield.py
new file mode 100644
index 00000000..7cba8e8e
--- /dev/null
+++ b/shopify/resources/metafield.py
@@ -0,0 +1,13 @@
+from ..base import ShopifyResource
+
+
+class Metafield(ShopifyResource):
+ _prefix_source = "/$resource/$resource_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ resource = options.get("resource")
+ if resource:
+ return "%s/%s/%s" % (cls.site, resource, options["resource_id"])
+ else:
+ return cls.site
diff --git a/shopify/resources/note_attribute.py b/shopify/resources/note_attribute.py
new file mode 100644
index 00000000..9e997e5e
--- /dev/null
+++ b/shopify/resources/note_attribute.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class NoteAttribute(ShopifyResource):
+ pass
diff --git a/shopify/resources/option.py b/shopify/resources/option.py
new file mode 100644
index 00000000..3ff640d5
--- /dev/null
+++ b/shopify/resources/option.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Option(ShopifyResource):
+ pass
diff --git a/shopify/resources/order.py b/shopify/resources/order.py
new file mode 100644
index 00000000..2e31a8c3
--- /dev/null
+++ b/shopify/resources/order.py
@@ -0,0 +1,30 @@
+from ..base import ShopifyResource
+from shopify import mixins
+from .transaction import Transaction
+
+
+class Order(ShopifyResource, mixins.Metafields, mixins.Events):
+ _prefix_source = "/customers/$customer_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ customer_id = options.get("customer_id")
+ if customer_id:
+ return "%s/customers/%s" % (cls.site, customer_id)
+ else:
+ return cls.site
+
+ def close(self):
+ self._load_attributes_from_response(self.post("close"))
+
+ def open(self):
+ self._load_attributes_from_response(self.post("open"))
+
+ def cancel(self, **kwargs):
+ self._load_attributes_from_response(self.post("cancel", **kwargs))
+
+ def transactions(self):
+ return Transaction.find(order_id=self.id)
+
+ def capture(self, amount=""):
+ return Transaction.create({"amount": amount, "kind": "capture", "order_id": self.id})
diff --git a/shopify/resources/order_risk.py b/shopify/resources/order_risk.py
new file mode 100644
index 00000000..fdcfa1f3
--- /dev/null
+++ b/shopify/resources/order_risk.py
@@ -0,0 +1,7 @@
+from ..base import ShopifyResource
+
+
+class OrderRisk(ShopifyResource):
+ _prefix_source = "/orders/$order_id/"
+ _singular = "risk"
+ _plural = "risks"
diff --git a/shopify/resources/page.py b/shopify/resources/page.py
new file mode 100644
index 00000000..aa2711b2
--- /dev/null
+++ b/shopify/resources/page.py
@@ -0,0 +1,6 @@
+from ..base import ShopifyResource
+from shopify import mixins
+
+
+class Page(ShopifyResource, mixins.Metafields, mixins.Events):
+ pass
diff --git a/shopify/resources/payment_details.py b/shopify/resources/payment_details.py
new file mode 100644
index 00000000..24bed6d0
--- /dev/null
+++ b/shopify/resources/payment_details.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class PaymentDetails(ShopifyResource):
+ pass
diff --git a/shopify/resources/payouts.py b/shopify/resources/payouts.py
new file mode 100644
index 00000000..dea162d8
--- /dev/null
+++ b/shopify/resources/payouts.py
@@ -0,0 +1,6 @@
+from ..base import ShopifyResource
+from shopify import mixins
+
+
+class Payouts(ShopifyResource, mixins.Metafields):
+ _prefix_source = "/shopify_payments/"
diff --git a/shopify/resources/policy.py b/shopify/resources/policy.py
new file mode 100644
index 00000000..d97fcc2e
--- /dev/null
+++ b/shopify/resources/policy.py
@@ -0,0 +1,7 @@
+from ..base import ShopifyResource
+from shopify import mixins
+import shopify
+
+
+class Policy(ShopifyResource, mixins.Metafields, mixins.Events):
+ pass
diff --git a/shopify/resources/price_rule.py b/shopify/resources/price_rule.py
new file mode 100644
index 00000000..41fe3e04
--- /dev/null
+++ b/shopify/resources/price_rule.py
@@ -0,0 +1,24 @@
+import json
+from ..base import ShopifyResource
+from .discount_code import DiscountCode
+from .discount_code_creation import DiscountCodeCreation
+
+
+class PriceRule(ShopifyResource):
+ def add_discount_code(self, discount_code=DiscountCode()):
+ resource = self.post("discount_codes", discount_code.encode())
+ return DiscountCode(PriceRule.format.decode(resource.body))
+
+ def discount_codes(self):
+ return DiscountCode.find(price_rule_id=self.id)
+
+ def create_batch(self, codes=[]):
+ codes_json = json.dumps({"discount_codes": codes})
+
+ response = self.post("batch", codes_json.encode())
+ return DiscountCodeCreation(PriceRule.format.decode(response.body))
+
+ def find_batch(self, batch_id):
+ return DiscountCodeCreation.find_one(
+ "%s/price_rules/%s/batch/%s.%s" % (ShopifyResource.site, self.id, batch_id, PriceRule.format.extension)
+ )
diff --git a/shopify/resources/product.py b/shopify/resources/product.py
new file mode 100644
index 00000000..cc16e3e3
--- /dev/null
+++ b/shopify/resources/product.py
@@ -0,0 +1,43 @@
+from ..base import ShopifyResource
+from shopify import mixins
+import shopify
+
+
+class Product(ShopifyResource, mixins.Metafields, mixins.Events):
+ def price_range(self):
+ prices = [float(variant.price) for variant in self.variants]
+ f = "%0.2f"
+ min_price = min(prices)
+ max_price = max(prices)
+ if min_price != max_price:
+ return "%s - %s" % (f % min_price, f % max_price)
+ else:
+ return f % min_price
+
+ def collections(self):
+ return shopify.CustomCollection.find(product_id=self.id)
+
+ def smart_collections(self):
+ return shopify.SmartCollection.find(product_id=self.id)
+
+ def add_to_collection(self, collection):
+ return collection.add_product(self)
+
+ def remove_from_collection(self, collection):
+ return collection.remove_product(self)
+
+ def add_variant(self, variant):
+ variant.attributes["product_id"] = self.id
+ return variant.save()
+
+ def save(self):
+ start_api_version = "201910"
+ api_version = ShopifyResource.version
+ if api_version and (api_version.strip("-") >= start_api_version) and api_version != "unstable":
+ if "variants" in self.attributes:
+ for variant in self.variants:
+ if "inventory_quantity" in variant.attributes:
+ del variant.attributes["inventory_quantity"]
+ if "old_inventory_quantity" in variant.attributes:
+ del variant.attributes["old_inventory_quantity"]
+ return super(ShopifyResource, self).save()
diff --git a/shopify/resources/product_listing.py b/shopify/resources/product_listing.py
new file mode 100644
index 00000000..3e59d6c1
--- /dev/null
+++ b/shopify/resources/product_listing.py
@@ -0,0 +1,9 @@
+from ..base import ShopifyResource
+
+
+class ProductListing(ShopifyResource):
+ _primary_key = "product_id"
+
+ @classmethod
+ def product_ids(cls, **kwargs):
+ return cls.get("product_ids", **kwargs)
diff --git a/shopify/resources/product_publication.py b/shopify/resources/product_publication.py
new file mode 100644
index 00000000..4ba510de
--- /dev/null
+++ b/shopify/resources/product_publication.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class ProductPublication(ShopifyResource):
+ _prefix_source = "/publications/$publication_id/"
diff --git a/shopify/resources/province.py b/shopify/resources/province.py
new file mode 100644
index 00000000..d34496f9
--- /dev/null
+++ b/shopify/resources/province.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Province(ShopifyResource):
+ _prefix_source = "/admin/countries/$country_id/"
diff --git a/shopify/resources/publication.py b/shopify/resources/publication.py
new file mode 100644
index 00000000..1b4079f4
--- /dev/null
+++ b/shopify/resources/publication.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Publication(ShopifyResource):
+ pass
diff --git a/shopify/resources/receipt.py b/shopify/resources/receipt.py
new file mode 100644
index 00000000..0dd86a39
--- /dev/null
+++ b/shopify/resources/receipt.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Receipt(ShopifyResource):
+ pass
diff --git a/shopify/resources/recurring_application_charge.py b/shopify/resources/recurring_application_charge.py
new file mode 100644
index 00000000..c94cac2d
--- /dev/null
+++ b/shopify/resources/recurring_application_charge.py
@@ -0,0 +1,28 @@
+from ..base import ShopifyResource
+from .usage_charge import UsageCharge
+
+
+def _get_first_by_status(resources, status):
+ for resource in resources:
+ if resource.status == status:
+ return resource
+ return None
+
+
+class RecurringApplicationCharge(ShopifyResource):
+ def usage_charges(self):
+ return UsageCharge.find(recurring_application_charge_id=self.id)
+
+ def customize(self, **kwargs):
+ self._load_attributes_from_response(self.put("customize", recurring_application_charge=kwargs))
+
+ @classmethod
+ def current(cls):
+ """
+ Returns first RecurringApplicationCharge object with status=active.
+ If not found, None will be returned.
+ """
+ return _get_first_by_status(cls.find(), "active")
+
+ def activate(self):
+ self._load_attributes_from_response(self.post("activate"))
diff --git a/shopify/resources/redirect.py b/shopify/resources/redirect.py
new file mode 100644
index 00000000..041ecb81
--- /dev/null
+++ b/shopify/resources/redirect.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Redirect(ShopifyResource):
+ pass
diff --git a/shopify/resources/refund.py b/shopify/resources/refund.py
new file mode 100644
index 00000000..124036b3
--- /dev/null
+++ b/shopify/resources/refund.py
@@ -0,0 +1,29 @@
+import json
+
+from ..base import ShopifyResource
+
+
+class Refund(ShopifyResource):
+ _prefix_source = "/orders/$order_id/"
+
+ @classmethod
+ def calculate(cls, order_id, shipping=None, refund_line_items=None):
+ """
+ Calculates refund transactions based on line items and shipping.
+ When you want to create a refund, you should first use the calculate
+ endpoint to generate accurate refund transactions.
+
+ Args:
+ order_id: Order ID for which the Refund has to created.
+ shipping: Specify how much shipping to refund.
+ refund_line_items: A list of line item IDs and quantities to refund.
+ Returns:
+ Unsaved refund record
+ """
+ data = {}
+ if shipping:
+ data["shipping"] = shipping
+ data["refund_line_items"] = refund_line_items or []
+ body = {"refund": data}
+ resource = cls.post("calculate", order_id=order_id, body=json.dumps(body).encode())
+ return cls(cls.format.decode(resource.body), prefix_options={"order_id": order_id})
diff --git a/shopify/resources/report.py b/shopify/resources/report.py
new file mode 100644
index 00000000..e01b4bbd
--- /dev/null
+++ b/shopify/resources/report.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Report(ShopifyResource):
+ pass
diff --git a/shopify/resources/resource_feedback.py b/shopify/resources/resource_feedback.py
new file mode 100644
index 00000000..0ead8f77
--- /dev/null
+++ b/shopify/resources/resource_feedback.py
@@ -0,0 +1,14 @@
+from ..base import ShopifyResource
+
+
+class ResourceFeedback(ShopifyResource):
+ _prefix_source = "/products/$product_id/"
+ _plural = "resource_feedback"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ product_id = options.get("product_id")
+ if product_id:
+ return "%s/products/%s" % (cls.site, product_id)
+ else:
+ return cls.site
diff --git a/shopify/resources/rule.py b/shopify/resources/rule.py
new file mode 100644
index 00000000..27c460c3
--- /dev/null
+++ b/shopify/resources/rule.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Rule(ShopifyResource):
+ pass
diff --git a/shopify/resources/script_tag.py b/shopify/resources/script_tag.py
new file mode 100644
index 00000000..b627778c
--- /dev/null
+++ b/shopify/resources/script_tag.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class ScriptTag(ShopifyResource):
+ pass
diff --git a/shopify/resources/shipping_address.py b/shopify/resources/shipping_address.py
new file mode 100644
index 00000000..283aff34
--- /dev/null
+++ b/shopify/resources/shipping_address.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class ShippingAddress(ShopifyResource):
+ pass
diff --git a/shopify/resources/shipping_line.py b/shopify/resources/shipping_line.py
new file mode 100644
index 00000000..1f0d4c2b
--- /dev/null
+++ b/shopify/resources/shipping_line.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class ShippingLine(ShopifyResource):
+ pass
diff --git a/shopify/resources/shipping_zone.py b/shopify/resources/shipping_zone.py
new file mode 100644
index 00000000..49cd647d
--- /dev/null
+++ b/shopify/resources/shipping_zone.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class ShippingZone(ShopifyResource):
+ pass
diff --git a/shopify/resources/shop.py b/shopify/resources/shop.py
new file mode 100644
index 00000000..4d447366
--- /dev/null
+++ b/shopify/resources/shop.py
@@ -0,0 +1,21 @@
+from ..base import ShopifyResource
+from .metafield import Metafield
+from .event import Event
+
+
+class Shop(ShopifyResource):
+ @classmethod
+ def current(cls):
+ return cls.find_one(cls.site + "/shop." + cls.format.extension)
+
+ def metafields(self):
+ return Metafield.find()
+
+ def add_metafield(self, metafield):
+ if self.is_new():
+ raise ValueError("You can only add metafields to a resource that has been saved")
+ metafield.save()
+ return metafield
+
+ def events(self):
+ return Event.find()
diff --git a/shopify/resources/smart_collection.py b/shopify/resources/smart_collection.py
new file mode 100644
index 00000000..802c3a7e
--- /dev/null
+++ b/shopify/resources/smart_collection.py
@@ -0,0 +1,8 @@
+from ..base import ShopifyResource
+from shopify import mixins
+import shopify
+
+
+class SmartCollection(ShopifyResource, mixins.Metafields, mixins.Events):
+ def products(self):
+ return shopify.Product.find(collection_id=self.id)
diff --git a/shopify/resources/storefront_access_token.py b/shopify/resources/storefront_access_token.py
new file mode 100644
index 00000000..f1132f49
--- /dev/null
+++ b/shopify/resources/storefront_access_token.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class StorefrontAccessToken(ShopifyResource):
+ pass
diff --git a/shopify/resources/tax_line.py b/shopify/resources/tax_line.py
new file mode 100644
index 00000000..b9cda0c7
--- /dev/null
+++ b/shopify/resources/tax_line.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class TaxLine(ShopifyResource):
+ pass
diff --git a/shopify/resources/tender_transaction.py b/shopify/resources/tender_transaction.py
new file mode 100644
index 00000000..0999ab6e
--- /dev/null
+++ b/shopify/resources/tender_transaction.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class TenderTransaction(ShopifyResource):
+ pass
diff --git a/shopify/resources/theme.py b/shopify/resources/theme.py
new file mode 100644
index 00000000..d0ea8655
--- /dev/null
+++ b/shopify/resources/theme.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Theme(ShopifyResource):
+ pass
diff --git a/shopify/resources/transaction.py b/shopify/resources/transaction.py
new file mode 100644
index 00000000..f465255a
--- /dev/null
+++ b/shopify/resources/transaction.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class Transaction(ShopifyResource):
+ _prefix_source = "/orders/$order_id/"
diff --git a/shopify/resources/transactions.py b/shopify/resources/transactions.py
new file mode 100644
index 00000000..90cb884f
--- /dev/null
+++ b/shopify/resources/transactions.py
@@ -0,0 +1,6 @@
+from ..base import ShopifyResource
+from shopify import mixins
+
+
+class Transactions(ShopifyResource, mixins.Metafields):
+ _prefix_source = "/shopify_payments/balance/"
diff --git a/shopify/resources/usage_charge.py b/shopify/resources/usage_charge.py
new file mode 100644
index 00000000..bd5cd757
--- /dev/null
+++ b/shopify/resources/usage_charge.py
@@ -0,0 +1,13 @@
+from ..base import ShopifyResource
+
+
+class UsageCharge(ShopifyResource):
+ _prefix_source = "/recurring_application_charge/$recurring_application_charge_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ recurring_application_charge_id = options.get("recurring_application_charge_id")
+ if recurring_application_charge_id:
+ return "%s/recurring_application_charges/%s" % (cls.site, recurring_application_charge_id)
+ else:
+ return cls.site
diff --git a/shopify/resources/user.py b/shopify/resources/user.py
new file mode 100644
index 00000000..a1b50cb5
--- /dev/null
+++ b/shopify/resources/user.py
@@ -0,0 +1,7 @@
+from ..base import ShopifyResource
+
+
+class User(ShopifyResource):
+ @classmethod
+ def current(cls):
+ return User(cls.get("current"))
diff --git a/shopify/resources/variant.py b/shopify/resources/variant.py
new file mode 100644
index 00000000..743b071b
--- /dev/null
+++ b/shopify/resources/variant.py
@@ -0,0 +1,28 @@
+from ..base import ShopifyResource
+from shopify import mixins
+
+
+class Variant(ShopifyResource, mixins.Metafields):
+ _prefix_source = "/products/$product_id/"
+
+ @classmethod
+ def _prefix(cls, options={}):
+ product_id = options.get("product_id")
+ if product_id:
+ return "%s/products/%s" % (cls.site, product_id)
+ else:
+ return cls.site
+
+ def save(self):
+ if "product_id" not in self._prefix_options:
+ self._prefix_options["product_id"] = self.product_id
+
+ start_api_version = "201910"
+ api_version = ShopifyResource.version
+ if api_version and (api_version.strip("-") >= start_api_version) and api_version != "unstable":
+ if "inventory_quantity" in self.attributes:
+ del self.attributes["inventory_quantity"]
+ if "old_inventory_quantity" in self.attributes:
+ del self.attributes["old_inventory_quantity"]
+
+ return super(ShopifyResource, self).save()
diff --git a/shopify/resources/webhook.py b/shopify/resources/webhook.py
new file mode 100644
index 00000000..ba8a7f28
--- /dev/null
+++ b/shopify/resources/webhook.py
@@ -0,0 +1,11 @@
+from ..base import ShopifyResource
+
+
+class Webhook(ShopifyResource):
+ def __get_format(self):
+ return self.attributes.get("format")
+
+ def __set_format(self, data):
+ self.attributes["format"] = data
+
+ format = property(__get_format, __set_format, None, "Format attribute")
diff --git a/shopify/session.py b/shopify/session.py
new file mode 100644
index 00000000..561faacf
--- /dev/null
+++ b/shopify/session.py
@@ -0,0 +1,185 @@
+import time
+import hmac
+import json
+from hashlib import sha256
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
+import re
+from contextlib import contextmanager
+from six.moves import urllib
+from shopify.api_access import ApiAccess
+from shopify.api_version import ApiVersion, Release, Unstable
+import six
+
+
+class ValidationException(Exception):
+ pass
+
+
+class Session(object):
+ api_key = None
+ secret = None
+ protocol = "https"
+ myshopify_domain = "myshopify.com"
+ port = None
+
+ @classmethod
+ def setup(cls, **kwargs):
+ for k, v in six.iteritems(kwargs):
+ setattr(cls, k, v)
+
+ @classmethod
+ @contextmanager
+ def temp(cls, domain, version, token):
+ import shopify
+
+ original_domain = shopify.ShopifyResource.url
+ original_token = shopify.ShopifyResource.get_headers().get("X-Shopify-Access-Token")
+ original_version = shopify.ShopifyResource.get_version() or version
+ original_session = shopify.Session(original_domain, original_version, original_token)
+
+ session = Session(domain, version, token)
+ shopify.ShopifyResource.activate_session(session)
+ yield
+ shopify.ShopifyResource.activate_session(original_session)
+
+ def __init__(self, shop_url, version=None, token=None, access_scopes=None):
+ self.url = self.__prepare_url(shop_url)
+ self.token = token
+ self.version = ApiVersion.coerce_to_version(version)
+ self.access_scopes = access_scopes
+ return
+
+ def create_permission_url(self, redirect_uri, scope=None, state=None):
+ query_params = {"client_id": self.api_key, "redirect_uri": redirect_uri}
+ # `scope` should be omitted if provided by app's TOML
+ if scope:
+ query_params["scope"] = ",".join(scope)
+ if state:
+ query_params["state"] = state
+ return "https://%s/admin/oauth/authorize?%s" % (self.url, urllib.parse.urlencode(query_params))
+
+ def request_token(self, params):
+ if self.token:
+ return self.token
+
+ if not self.validate_params(params):
+ raise ValidationException("Invalid HMAC: Possibly malicious login")
+
+ code = params["code"]
+
+ url = "https://%s/admin/oauth/access_token?" % self.url
+ query_params = {"client_id": self.api_key, "client_secret": self.secret, "code": code}
+ request = urllib.request.Request(url, urllib.parse.urlencode(query_params).encode("utf-8"))
+ response = urllib.request.urlopen(request)
+
+ if response.code == 200:
+ json_payload = json.loads(response.read().decode("utf-8"))
+ self.token = json_payload["access_token"]
+ self.access_scopes = json_payload["scope"]
+
+ return self.token
+ else:
+ raise Exception(response.msg)
+
+ @property
+ def api_version(self):
+ return self.version
+
+ @property
+ def site(self):
+ return self.version.api_path("%s://%s" % (self.protocol, self.url))
+
+ @property
+ def valid(self):
+ return self.url is not None and self.token is not None
+
+ @property
+ def access_scopes(self):
+ return self._access_scopes
+
+ @access_scopes.setter
+ def access_scopes(self, scopes):
+ if scopes is None or type(scopes) == ApiAccess:
+ self._access_scopes = scopes
+ else:
+ self._access_scopes = ApiAccess(scopes)
+
+ @classmethod
+ def __prepare_url(cls, url):
+ if not url or (url.strip() == ""):
+ return None
+ url = re.sub("^https?://", "", url)
+ shop = urllib.parse.urlparse("https://" + url).hostname
+ if shop is None:
+ return None
+ idx = shop.find(".")
+ if idx != -1:
+ shop = shop[0:idx]
+ if len(shop) == 0:
+ return None
+ shop += "." + cls.myshopify_domain
+ if cls.port:
+ shop += ":" + str(cls.port)
+ return shop
+
+ @classmethod
+ def validate_params(cls, params):
+ # Avoid replay attacks by making sure the request
+ # isn't more than a day old.
+ one_day = 24 * 60 * 60
+ if int(params.get("timestamp", 0)) < time.time() - one_day:
+ return False
+
+ return cls.validate_hmac(params)
+
+ @classmethod
+ def validate_hmac(cls, params):
+ if "hmac" not in params:
+ return False
+
+ hmac_calculated = cls.calculate_hmac(params).encode("utf-8")
+ hmac_to_verify = params["hmac"].encode("utf-8")
+
+ # Try to use compare_digest() to reduce vulnerability to timing attacks.
+ # If it's not available, just fall back to regular string comparison.
+ try:
+ return hmac.compare_digest(hmac_calculated, hmac_to_verify)
+ except AttributeError:
+ return hmac_calculated == hmac_to_verify
+
+ @classmethod
+ def calculate_hmac(cls, params):
+ """
+ Calculate the HMAC of the given parameters in line with Shopify's rules for OAuth authentication.
+ See http://docs.shopify.com/api/authentication/oauth#verification.
+ """
+ encoded_params = cls.__encoded_params_for_signature(params)
+ # Generate the hex digest for the sorted parameters using the secret.
+ return hmac.new(cls.secret.encode(), encoded_params.encode(), sha256).hexdigest()
+
+ @classmethod
+ def __encoded_params_for_signature(cls, params):
+ """
+ Sort and combine query parameters into a single string, excluding those that should be removed and joining with '&'
+ """
+
+ def encoded_pairs(params):
+ for k, v in six.iteritems(params):
+ if k == "hmac":
+ continue
+
+ if k.endswith("[]"):
+ # foo[]=1&foo[]=2 has to be transformed as foo=["1", "2"] note the whitespace after comma
+ k = k.rstrip("[]")
+ v = json.dumps(list(map(str, v)))
+
+ # escape delimiters to avoid tampering
+ k = str(k).replace("%", "%25").replace("=", "%3D")
+ v = str(v).replace("%", "%25")
+ yield "{0}={1}".format(k, v).replace("&", "%26")
+
+ return "&".join(sorted(encoded_pairs(params)))
diff --git a/shopify/session_token.py b/shopify/session_token.py
new file mode 100644
index 00000000..91a4970b
--- /dev/null
+++ b/shopify/session_token.py
@@ -0,0 +1,84 @@
+import jwt
+import re
+import six
+import sys
+
+from shopify.utils import shop_url
+
+if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0
+ from urlparse import urljoin
+else:
+ from urllib.parse import urljoin
+
+
+ALGORITHM = "HS256"
+PREFIX = "Bearer "
+REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"]
+LEEWAY_SECONDS = 10
+
+
+class SessionTokenError(Exception):
+ pass
+
+
+class InvalidIssuerError(SessionTokenError):
+ pass
+
+
+class MismatchedHostsError(SessionTokenError):
+ pass
+
+
+class TokenAuthenticationError(SessionTokenError):
+ pass
+
+
+def decode_from_header(authorization_header, api_key, secret):
+ session_token = _extract_session_token(authorization_header)
+ decoded_payload = _decode_session_token(session_token, api_key, secret)
+ _validate_issuer(decoded_payload)
+
+ return decoded_payload
+
+
+def _extract_session_token(authorization_header):
+ if not authorization_header.startswith(PREFIX):
+ raise TokenAuthenticationError("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token")
+
+ return authorization_header[len(PREFIX) :]
+
+
+def _decode_session_token(session_token, api_key, secret):
+ try:
+ return jwt.decode(
+ session_token,
+ secret,
+ audience=api_key,
+ algorithms=[ALGORITHM],
+ # AppBridge frequently sends future `nbf`, and it causes `ImmatureSignatureError`.
+ # Accept few seconds clock skew to avoid this error.
+ leeway=LEEWAY_SECONDS,
+ options={"require": REQUIRED_FIELDS},
+ )
+ except jwt.exceptions.PyJWTError as exception:
+ six.raise_from(SessionTokenError(str(exception)), exception)
+
+
+def _validate_issuer(decoded_payload):
+ _validate_issuer_hostname(decoded_payload)
+ _validate_issuer_and_dest_match(decoded_payload)
+
+
+def _validate_issuer_hostname(decoded_payload):
+ issuer_root = urljoin(decoded_payload["iss"], "/")
+
+ if not shop_url.sanitize_shop_domain(issuer_root):
+ raise InvalidIssuerError("Invalid issuer")
+
+
+def _validate_issuer_and_dest_match(decoded_payload):
+ issuer_root = urljoin(decoded_payload["iss"], "/")
+ dest_root = urljoin(decoded_payload["dest"], "/")
+
+ if issuer_root != dest_root:
+ raise MismatchedHostsError("The issuer and destination do not match")
diff --git a/shopify/utils/__init__.py b/shopify/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/shopify/utils/shop_url.py b/shopify/utils/shop_url.py
new file mode 100644
index 00000000..76e088a7
--- /dev/null
+++ b/shopify/utils/shop_url.py
@@ -0,0 +1,20 @@
+import re
+import sys
+
+if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0
+ from urlparse import urlparse
+else:
+ from urllib.parse import urlparse
+
+HOSTNAME_PATTERN = r"[a-z0-9][a-z0-9-]*[a-z0-9]"
+
+
+def sanitize_shop_domain(shop_domain, myshopify_domain="myshopify.com"):
+ name = str(shop_domain or "").lower().strip()
+ if myshopify_domain not in name and "." not in name:
+ name += ".{domain}".format(domain=myshopify_domain)
+ name = re.sub(r"https?://", "", name)
+
+ uri = urlparse("http://{hostname}".format(hostname=name))
+ if re.match(r"{h}\.{d}$".format(h=HOSTNAME_PATTERN, d=re.escape(myshopify_domain)), uri.netloc):
+ return uri.netloc
diff --git a/shopify/version.py b/shopify/version.py
new file mode 100644
index 00000000..dfb0b4e4
--- /dev/null
+++ b/shopify/version.py
@@ -0,0 +1 @@
+VERSION = "12.7.1"
diff --git a/lib/yamlobjects.py b/shopify/yamlobjects.py
similarity index 90%
rename from lib/yamlobjects.py
rename to shopify/yamlobjects.py
index d4e7cd3a..c7438c42 100644
--- a/lib/yamlobjects.py
+++ b/shopify/yamlobjects.py
@@ -8,11 +8,12 @@
import yaml
class YAMLHashWithIndifferentAccess(yaml.YAMLObject):
- yaml_tag = '!map:ActiveSupport::HashWithIndifferentAccess'
+ yaml_tag = "!map:ActiveSupport::HashWithIndifferentAccess"
yaml_loader = yaml.SafeLoader
@classmethod
def from_yaml(cls, loader, node):
return loader.construct_mapping(node, cls)
+
except ImportError:
pass
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/test/access_scope_test.py b/test/access_scope_test.py
new file mode 100644
index 00000000..e931263a
--- /dev/null
+++ b/test/access_scope_test.py
@@ -0,0 +1,10 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class AccessScopeTest(TestCase):
+ def test_find_should_return_all_access_scopes(self):
+ self.fake("oauth/access_scopes", body=self.load_fixture("access_scopes"), prefix="/admin")
+ scopes = shopify.AccessScope.find()
+ self.assertEqual(3, len(scopes))
+ self.assertEqual("read_products", scopes[0].handle)
diff --git a/test/api_access_test.py b/test/api_access_test.py
new file mode 100644
index 00000000..21866a09
--- /dev/null
+++ b/test/api_access_test.py
@@ -0,0 +1,153 @@
+from shopify import ApiAccess, ApiAccessError
+from test.test_helper import TestCase
+
+
+class ApiAccessTest(TestCase):
+ def test_creating_scopes_from_a_string_works_with_a_comma_separated_list(self):
+ deserialized_read_products_write_orders = ApiAccess("read_products,write_orders")
+ serialized_read_products_write_orders = str(deserialized_read_products_write_orders)
+ expected_read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(expected_read_products_write_orders, ApiAccess(serialized_read_products_write_orders))
+
+ def test_creating_api_access_from_invalid_scopes_raises(self):
+ with self.assertRaises(ApiAccessError) as cm:
+ api_access = ApiAccess("bad_scope, read_orders,write_orders")
+
+ self.assertEqual("'bad_scope' is not a valid access scope", str(cm.exception))
+
+ def test_returns_list_of_reduced_scopes(self):
+ api_access = ApiAccess("read_products, read_orders,write_orders")
+ expected_scopes = set(["read_products", "write_orders"])
+ scopes = list(api_access)
+
+ self.assertEqual(expected_scopes, set(scopes))
+
+ def test_write_is_the_same_access_as_read_write_on_the_same_resource(self):
+ read_write_orders = ApiAccess(["read_orders", "write_orders"])
+ write_orders = ApiAccess("write_orders")
+
+ self.assertEqual(write_orders, read_write_orders)
+
+ def test_write_is_the_same_access_as_read_write_on_the_same_unauthenticated_resource(self):
+ unauthenticated_read_write_orders = ApiAccess(["unauthenticated_read_orders", "unauthenticated_write_orders"])
+ unauthenticated_write_orders = ApiAccess("unauthenticated_write_orders")
+
+ self.assertEqual(unauthenticated_write_orders, unauthenticated_read_write_orders)
+
+ def test_read_is_not_the_same_as_read_write_on_the_same_resource(self):
+ read_orders = ApiAccess("read_orders")
+ read_write_orders = ApiAccess(["write_orders", "read_orders"])
+
+ self.assertNotEqual(read_write_orders, read_orders)
+
+ def test_two_different_resources_are_not_equal(self):
+ read_orders = ApiAccess("read_orders")
+ read_products = ApiAccess("read_products")
+
+ self.assertNotEqual(read_orders, read_products)
+
+ def test_two_identical_scopes_are_equal(self):
+ read_orders = ApiAccess("read_orders")
+ read_orders_identical = ApiAccess("read_orders")
+
+ self.assertEqual(read_orders, read_orders_identical)
+
+ def test_unauthenticated_is_not_implied_by_authenticated_access(self):
+ unauthenticated_orders = ApiAccess("unauthenticated_read_orders")
+ authenticated_read_orders = ApiAccess("read_orders")
+ authenticated_write_orders = ApiAccess("write_orders")
+
+ self.assertNotEqual(unauthenticated_orders, authenticated_read_orders)
+ self.assertNotEqual(unauthenticated_orders, authenticated_write_orders)
+
+ def test_scopes_covers_is_truthy_for_same_scopes(self):
+ read_orders = ApiAccess("read_orders")
+ read_orders_identical = ApiAccess("read_orders")
+
+ self.assertTrue(read_orders.covers(read_orders_identical))
+
+ def test_covers_is_falsy_for_different_scopes(self):
+ read_orders = ApiAccess("read_orders")
+ read_products = ApiAccess("read_products")
+
+ self.assertFalse(read_orders.covers(read_products))
+
+ def test_covers_is_truthy_for_read_when_the_set_has_read_write(self):
+ write_products = ApiAccess("write_products")
+ read_products = ApiAccess("read_products")
+
+ self.assertTrue(write_products.covers(read_products))
+
+ def test_covers_is_truthy_for_read_when_the_set_has_read_write_for_that_resource_and_others(self):
+ write_products_and_orders = ApiAccess(["write_products", "write_orders"])
+ read_orders = ApiAccess("read_orders")
+
+ self.assertTrue(write_products_and_orders.covers(read_orders))
+
+ def test_covers_is_truthy_for_write_when_the_set_has_read_write_for_that_resource_and_others(self):
+ write_products_and_orders = ApiAccess(["write_products", "write_orders"])
+ write_orders = ApiAccess("write_orders")
+
+ self.assertTrue(write_products_and_orders.covers(write_orders))
+
+ def test_covers_is_truthy_for_subset_of_scopes(self):
+ write_products_orders_customers = ApiAccess(["write_products", "write_orders", "write_customers"])
+ write_orders_products = ApiAccess(["write_orders", "read_products"])
+
+ self.assertTrue(write_products_orders_customers.covers(write_orders_products))
+
+ def test_covers_is_falsy_for_sets_of_scopes_that_have_no_common_elements(self):
+ write_products_orders_customers = ApiAccess(["write_products", "write_orders", "write_customers"])
+ write_images_read_content = ApiAccess(["write_images", "read_content"])
+
+ self.assertFalse(write_products_orders_customers.covers(write_images_read_content))
+
+ def test_covers_is_falsy_for_sets_of_scopes_that_have_only_some_common_access(self):
+ write_products_orders_customers = ApiAccess(["write_products", "write_orders", "write_customers"])
+ write_products_read_content = ApiAccess(["write_products", "read_content"])
+
+ self.assertFalse(write_products_orders_customers.covers(write_products_read_content))
+
+ def test_duplicate_scopes_resolve_to_one_scope(self):
+ read_orders_duplicated = ApiAccess(["read_orders", "read_orders", "read_orders", "read_orders"])
+ read_orders = ApiAccess("read_orders")
+
+ self.assertEqual(read_orders, read_orders_duplicated)
+
+ def test_to_s_outputs_scopes_as_a_comma_separated_list_without_implied_read_scopes(self):
+ serialized_read_products_write_orders = "read_products,write_orders"
+ read_products_write_orders = ApiAccess(["read_products", "read_orders", "write_orders"])
+
+ self.assertIn("read_products", str(read_products_write_orders))
+ self.assertIn("write_orders", str(read_products_write_orders))
+
+ def test_to_a_outputs_scopes_as_an_array_of_strings_without_implied_read_scopes(self):
+ serialized_read_products_write_orders = ["write_orders", "read_products"]
+ read_products_write_orders = ApiAccess(["read_products", "read_orders", "write_orders"])
+
+ self.assertEqual(set(serialized_read_products_write_orders), set(list(read_products_write_orders)))
+
+ def test_creating_scopes_removes_extra_whitespace_from_scope_name_and_blank_scope_names(self):
+ deserialized_read_products_write_orders = ApiAccess([" read_products", " ", "write_orders "])
+ serialized_read_products_write_orders = str(deserialized_read_products_write_orders)
+ expected_read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(expected_read_products_write_orders, ApiAccess(serialized_read_products_write_orders))
+
+ def test_creating_scopes_from_a_string_works_with_a_comma_separated_list(self):
+ deserialized_read_products_write_orders = ApiAccess("read_products,write_orders")
+ serialized_read_products_write_orders = str(deserialized_read_products_write_orders)
+ expected_read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(expected_read_products_write_orders, ApiAccess(serialized_read_products_write_orders))
+
+ def test_using_to_s_from_one_scopes_to_construct_another_will_be_equal(self):
+ read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(read_products_write_orders, ApiAccess(str(read_products_write_orders)))
+
+ def test_using_to_a_from_one_scopes_to_construct_another_will_be_equal(self):
+ read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(read_products_write_orders, ApiAccess(list(read_products_write_orders)))
diff --git a/test/api_permission_test.py b/test/api_permission_test.py
new file mode 100644
index 00000000..1e93ee74
--- /dev/null
+++ b/test/api_permission_test.py
@@ -0,0 +1,9 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class ApiPermissionTest(TestCase):
+ def test_delete_api_permission(self):
+ self.fake("api_permissions/current", method="DELETE", code=200, body="{}")
+
+ shopify.ApiPermission.delete()
diff --git a/test/api_version_test.py b/test/api_version_test.py
new file mode 100644
index 00000000..9dce8cb2
--- /dev/null
+++ b/test/api_version_test.py
@@ -0,0 +1,61 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class ApiVersionTest(TestCase):
+ """
+ Api Version Tests
+ """
+
+ def tearDown(self):
+ shopify.ApiVersion.clear_defined_versions()
+ shopify.ApiVersion.define_known_versions()
+
+ def test_unstable_api_path_returns_correct_url(self):
+ self.assertEqual(
+ "https://fakeshop.myshopify.com/admin/api/unstable",
+ shopify.Unstable().api_path("https://fakeshop.myshopify.com"),
+ )
+
+ def test_coerce_to_version_returns_known_versions(self):
+ v1 = shopify.Unstable()
+ v2 = shopify.ApiVersion.define_version(shopify.Release("2019-01"))
+
+ self.assertNotEqual(v1, None)
+ self.assertEqual(v1, shopify.ApiVersion.coerce_to_version("unstable"))
+ self.assertEqual(v2, shopify.ApiVersion.coerce_to_version("2019-01"))
+
+ def test_coerce_to_version_raises_with_string_that_does_not_match_known_version(self):
+ with self.assertRaises(shopify.VersionNotFoundError):
+ shopify.ApiVersion.coerce_to_version("crazy-name")
+
+ def test_coerce_to_version_creates_new_release_on_the_fly(self):
+ new_version = "2025-01"
+ coerced_version = shopify.ApiVersion.coerce_to_version(new_version)
+
+ self.assertIsInstance(coerced_version, shopify.Release)
+ self.assertEqual(coerced_version.name, new_version)
+ self.assertEqual(
+ coerced_version.api_path("https://test.myshopify.com"),
+ f"https://test.myshopify.com/admin/api/{new_version}",
+ )
+
+ # Verify that the new version is not added to the known versions
+ self.assertNotIn(new_version, shopify.ApiVersion.versions)
+
+
+class ReleaseTest(TestCase):
+ def test_raises_if_format_invalid(self):
+ with self.assertRaises(shopify.InvalidVersionError):
+ shopify.Release("crazy-name")
+
+ def test_release_api_path_returns_correct_url(self):
+ self.assertEqual(
+ "https://fakeshop.myshopify.com/admin/api/2019-04",
+ shopify.Release("2019-04").api_path("https://fakeshop.myshopify.com"),
+ )
+
+ def test_two_release_versions_with_same_number_are_equal(self):
+ version1 = shopify.Release("2019-01")
+ version2 = shopify.Release("2019-01")
+ self.assertEqual(version1, version2)
diff --git a/test/application_credit_test.py b/test/application_credit_test.py
new file mode 100644
index 00000000..23656b60
--- /dev/null
+++ b/test/application_credit_test.py
@@ -0,0 +1,33 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class ApplicationCreditTest(TestCase):
+ def test_get_application_credit(self):
+ self.fake("application_credits/445365009", method="GET", body=self.load_fixture("application_credit"), code=200)
+ application_credit = shopify.ApplicationCredit.find(445365009)
+ self.assertEqual("5.00", application_credit.amount)
+
+ def test_get_all_application_credits(self):
+ self.fake("application_credits", method="GET", body=self.load_fixture("application_credits"), code=200)
+ application_credits = shopify.ApplicationCredit.find()
+ self.assertEqual(1, len(application_credits))
+ self.assertEqual(445365009, application_credits[0].id)
+
+ def test_create_application_credit(self):
+ self.fake(
+ "application_credits",
+ method="POST",
+ body=self.load_fixture("application_credit"),
+ headers={"Content-type": "application/json"},
+ code=201,
+ )
+
+ application_credit = shopify.ApplicationCredit.create(
+ {"description": "application credit for refund", "amount": 5.0}
+ )
+
+ expected_body = {"application_credit": {"description": "application credit for refund", "amount": 5.0}}
+
+ self.assertEqual(expected_body, json.loads(self.http.request.data.decode("utf-8")))
diff --git a/test/article_test.py b/test/article_test.py
new file mode 100644
index 00000000..5ee53bbe
--- /dev/null
+++ b/test/article_test.py
@@ -0,0 +1,75 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class ArticleTest(TestCase):
+ def test_create_article(self):
+ self.fake(
+ "blogs/1008414260/articles",
+ method="POST",
+ body=self.load_fixture("article"),
+ headers={"Content-type": "application/json"},
+ )
+ article = shopify.Article({"blog_id": 1008414260})
+ article.save()
+ self.assertEqual("First Post", article.title)
+
+ def test_get_article(self):
+ self.fake("articles/6242736", method="GET", body=self.load_fixture("article"))
+ article = shopify.Article.find(6242736)
+ self.assertEqual("First Post", article.title)
+
+ def test_update_article(self):
+ self.fake("articles/6242736", method="GET", body=self.load_fixture("article"))
+ article = shopify.Article.find(6242736)
+
+ self.fake(
+ "articles/6242736",
+ method="PUT",
+ body=self.load_fixture("article"),
+ headers={"Content-type": "application/json"},
+ )
+ article.save()
+
+ def test_get_articles(self):
+ self.fake("articles", method="GET", body=self.load_fixture("articles"))
+ articles = shopify.Article.find()
+ self.assertEqual(3, len(articles))
+
+ def test_get_articles_namespaced(self):
+ self.fake("blogs/1008414260/articles", method="GET", body=self.load_fixture("articles"))
+ articles = shopify.Article.find(blog_id=1008414260)
+ self.assertEqual(3, len(articles))
+
+ def test_get_article_namespaced(self):
+ self.fake("blogs/1008414260/articles/6242736", method="GET", body=self.load_fixture("article"))
+ article = shopify.Article.find(6242736, blog_id=1008414260)
+ self.assertEqual("First Post", article.title)
+
+ def test_get_authors(self):
+ self.fake("articles/authors", method="GET", body=self.load_fixture("authors"))
+ authors = shopify.Article.authors()
+ self.assertEqual("Shopify", authors[0])
+ self.assertEqual("development shop", authors[-1])
+
+ def test_get_authors_for_blog_id(self):
+ self.fake("blogs/1008414260/articles/authors", method="GET", body=self.load_fixture("authors"))
+ authors = shopify.Article.authors(blog_id=1008414260)
+ self.assertEqual(3, len(authors))
+
+ def test_get_tags(self):
+ self.fake("articles/tags", method="GET", body=self.load_fixture("tags"))
+ tags = shopify.Article.tags()
+ self.assertEqual("consequuntur", tags[0])
+ self.assertEqual("repellendus", tags[-1])
+
+ def test_get_tags_for_blog_id(self):
+ self.fake("blogs/1008414260/articles/tags", method="GET", body=self.load_fixture("tags"))
+ tags = shopify.Article.tags(blog_id=1008414260)
+ self.assertEqual("consequuntur", tags[0])
+ self.assertEqual("repellendus", tags[-1])
+
+ def test_get_popular_tags(self):
+ self.fake("articles/tags.json?limit=1&popular=1", extension=False, method="GET", body=self.load_fixture("tags"))
+ tags = shopify.Article.tags(popular=1, limit=1)
+ self.assertEqual(3, len(tags))
diff --git a/test/asset_test.py b/test/asset_test.py
new file mode 100644
index 00000000..19606a4b
--- /dev/null
+++ b/test/asset_test.py
@@ -0,0 +1,90 @@
+import base64
+
+import shopify
+from test.test_helper import TestCase
+
+
+class AssetTest(TestCase):
+ def test_get_assets(self):
+ self.fake("assets", method="GET", body=self.load_fixture("assets"))
+ v = shopify.Asset.find()
+
+ def test_get_asset(self):
+ self.fake(
+ "assets.json?asset%5Bkey%5D=templates%2Findex.liquid",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid")
+
+ def test_update_asset(self):
+ self.fake(
+ "assets.json?asset%5Bkey%5D=templates%2Findex.liquid",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid")
+
+ self.fake("assets", method="PUT", body=self.load_fixture("asset"), headers={"Content-type": "application/json"})
+ v.save()
+
+ def test_get_assets_namespaced(self):
+ self.fake("themes/1/assets", method="GET", body=self.load_fixture("assets"))
+ v = shopify.Asset.find(theme_id=1)
+
+ def test_get_asset_namespaced(self):
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid", theme_id=1)
+
+ def test_update_asset_namespaced(self):
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid", theme_id=1)
+
+ self.fake(
+ "themes/1/assets",
+ method="PUT",
+ body=self.load_fixture("asset"),
+ headers={"Content-type": "application/json"},
+ )
+ v.save()
+
+ def test_delete_asset_namespaced(self):
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid", theme_id=1)
+
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid", extension=False, method="DELETE", body="{}"
+ )
+ v.destroy()
+
+ def test_attach(self):
+ self.fake(
+ "themes/1/assets",
+ method="PUT",
+ body=self.load_fixture("asset"),
+ headers={"Content-type": "application/json"},
+ )
+ attachment = b"dGVzdCBiaW5hcnkgZGF0YTogAAE="
+ key = "assets/test.jpeg"
+ theme_id = 1
+ asset = shopify.Asset({"key": key, "theme_id": theme_id})
+ asset.attach(attachment)
+ asset.save()
+ self.assertEqual(base64.b64encode(attachment).decode(), asset.attributes["attachment"])
diff --git a/test/balance_test.py b/test/balance_test.py
new file mode 100644
index 00000000..2cb26b08
--- /dev/null
+++ b/test/balance_test.py
@@ -0,0 +1,11 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class BalanceTest(TestCase):
+ prefix = "/admin/api/unstable/shopify_payments"
+
+ def test_get_balance(self):
+ self.fake("balance", method="GET", prefix=self.prefix, body=self.load_fixture("balance"))
+ balance = shopify.Balance.find()
+ self.assertGreater(len(balance), 0)
diff --git a/test/base_test.py b/test/base_test.py
new file mode 100644
index 00000000..5cc19a60
--- /dev/null
+++ b/test/base_test.py
@@ -0,0 +1,114 @@
+import shopify
+from test.test_helper import TestCase
+from pyactiveresource.activeresource import ActiveResource
+from mock import patch
+import threading
+
+
+class BaseTest(TestCase):
+ @classmethod
+ def setUpClass(self):
+ shopify.ApiVersion.define_known_versions()
+ shopify.ApiVersion.define_version(shopify.Release("2019-04"))
+ self.session1 = shopify.Session("shop1.myshopify.com", "unstable", "token1")
+ self.session2 = shopify.Session("shop2.myshopify.com", "2019-04", "token2")
+
+ @classmethod
+ def tearDownClass(self):
+ shopify.ApiVersion.clear_defined_versions()
+
+ def tearDown(self):
+ shopify.ShopifyResource.clear_session()
+
+ def test_activate_session_should_set_site_and_headers_for_given_session(self):
+ shopify.ShopifyResource.activate_session(self.session1)
+
+ self.assertIsNone(ActiveResource.site)
+ self.assertEqual("https://shop1.myshopify.com/admin/api/unstable", shopify.ShopifyResource.site)
+ self.assertEqual("https://shop1.myshopify.com/admin/api/unstable", shopify.Shop.site)
+ self.assertIsNone(ActiveResource.headers)
+ self.assertEqual("token1", shopify.ShopifyResource.headers["X-Shopify-Access-Token"])
+ self.assertEqual("token1", shopify.Shop.headers["X-Shopify-Access-Token"])
+
+ def test_activate_session_should_set_site_given_version(self):
+ shopify.ShopifyResource.activate_session(self.session2)
+
+ self.assertIsNone(ActiveResource.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.ShopifyResource.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.Shop.site)
+ self.assertIsNone(ActiveResource.headers)
+
+ def test_clear_session_should_clear_site_and_headers_from_base(self):
+ shopify.ShopifyResource.activate_session(self.session1)
+ shopify.ShopifyResource.clear_session()
+
+ self.assertIsNone(ActiveResource.site)
+ self.assertIsNone(shopify.ShopifyResource.site)
+ self.assertIsNone(shopify.Shop.site)
+
+ self.assertIsNone(ActiveResource.headers)
+ self.assertFalse("X-Shopify-Access-Token" in shopify.ShopifyResource.headers)
+ self.assertFalse("X-Shopify-Access-Token" in shopify.Shop.headers)
+
+ def test_activate_session_with_one_session_then_clearing_and_activating_with_another_session_shoul_request_to_correct_shop(
+ self,
+ ):
+ shopify.ShopifyResource.activate_session(self.session1)
+ shopify.ShopifyResource.clear_session()
+ shopify.ShopifyResource.activate_session(self.session2)
+
+ self.assertIsNone(ActiveResource.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.ShopifyResource.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.Shop.site)
+
+ self.assertIsNone(ActiveResource.headers)
+ self.assertEqual("token2", shopify.ShopifyResource.headers["X-Shopify-Access-Token"])
+ self.assertEqual("token2", shopify.Shop.headers["X-Shopify-Access-Token"])
+
+ def test_delete_should_send_custom_headers_with_request(self):
+ shopify.ShopifyResource.activate_session(self.session1)
+
+ org_headers = shopify.ShopifyResource.headers
+ shopify.ShopifyResource.set_headers({"X-Custom": "abc"})
+
+ with patch("shopify.ShopifyResource.connection.delete") as mock:
+ url = shopify.ShopifyResource._custom_method_collection_url("1", {})
+ shopify.ShopifyResource.delete("1")
+ mock.assert_called_with(url, {"X-Custom": "abc"})
+
+ shopify.ShopifyResource.set_headers(org_headers)
+
+ def test_headers_includes_user_agent(self):
+ self.assertTrue("User-Agent" in shopify.ShopifyResource.headers)
+ t = threading.Thread(target=lambda: self.assertTrue("User-Agent" in shopify.ShopifyResource.headers))
+ t.start()
+ t.join()
+
+ def test_headers_is_thread_safe(self):
+ def testFunc():
+ shopify.ShopifyResource.headers["X-Custom"] = "abc"
+ self.assertTrue("X-Custom" in shopify.ShopifyResource.headers)
+
+ t1 = threading.Thread(target=testFunc)
+ t1.start()
+ t1.join()
+
+ t2 = threading.Thread(target=lambda: self.assertFalse("X-Custom" in shopify.ShopifyResource.headers))
+ t2.start()
+ t2.join()
+
+ def test_setting_with_user_and_pass_strips_them(self):
+ shopify.ShopifyResource.clear_session()
+ self.fake(
+ "shop",
+ url="https://this-is-my-test-show.myshopify.com/admin/shop.json",
+ method="GET",
+ body=self.load_fixture("shop"),
+ headers={"Authorization": "Basic dXNlcjpwYXNz"},
+ )
+ API_KEY = "user"
+ PASSWORD = "pass"
+ shop_url = "https://%s:%s@this-is-my-test-show.myshopify.com/admin" % (API_KEY, PASSWORD)
+ shopify.ShopifyResource.set_site(shop_url)
+ res = shopify.Shop.current()
+ self.assertEqual("Apple Computers", res.name)
diff --git a/test/blog_test.py b/test/blog_test.py
new file mode 100644
index 00000000..e3a912f0
--- /dev/null
+++ b/test/blog_test.py
@@ -0,0 +1,15 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class BlogTest(TestCase):
+ def test_blog_creation(self):
+ self.fake(
+ "blogs",
+ method="POST",
+ code=202,
+ body=self.load_fixture("blog"),
+ headers={"Content-type": "application/json"},
+ )
+ blog = shopify.Blog.create({"title": "Test Blog"})
+ self.assertEqual("Test Blog", blog.title)
diff --git a/test/carrier_service_test.py b/test/carrier_service_test.py
new file mode 100644
index 00000000..a5aea4f6
--- /dev/null
+++ b/test/carrier_service_test.py
@@ -0,0 +1,26 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class CarrierServiceTest(TestCase):
+ def test_create_new_carrier_service(self):
+ self.fake(
+ "carrier_services",
+ method="POST",
+ body=self.load_fixture("carrier_service"),
+ headers={"Content-type": "application/json"},
+ )
+
+ carrier_service = shopify.CarrierService.create({"name": "Some Postal Service"})
+ self.assertEqual("Some Postal Service", carrier_service.name)
+
+ def test_get_carrier_service(self):
+ self.fake("carrier_services/123456", method="GET", body=self.load_fixture("carrier_service"))
+
+ carrier_service = shopify.CarrierService.find(123456)
+ self.assertEqual("Some Postal Service", carrier_service.name)
+
+ def test_set_format_attribute(self):
+ carrier_service = shopify.CarrierService()
+ carrier_service.format = "json"
+ self.assertEqual("json", carrier_service.attributes["format"])
diff --git a/test/cart_test.py b/test/cart_test.py
new file mode 100644
index 00000000..67836330
--- /dev/null
+++ b/test/cart_test.py
@@ -0,0 +1,13 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class CartTest(TestCase):
+ def test_all_should_return_all_carts(self):
+ self.fake("carts")
+ carts = shopify.Cart.find()
+ self.assertEqual(2, len(carts))
+ self.assertEqual(2, carts[0].id)
+ self.assertEqual("3eed8183d4281db6ea82ee2b8f23e9cc", carts[0].token)
+ self.assertEqual(1, len(carts[0].line_items))
+ self.assertEqual("test", carts[0].line_items[0].title)
diff --git a/test/checkout_test.py b/test/checkout_test.py
new file mode 100644
index 00000000..5b21d560
--- /dev/null
+++ b/test/checkout_test.py
@@ -0,0 +1,12 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class CheckoutTest(TestCase):
+ def test_all_should_return_all_checkouts(self):
+ self.fake("checkouts")
+ checkouts = shopify.Checkout.find()
+ self.assertEqual(1, len(checkouts))
+ self.assertEqual(450789469, checkouts[0].id)
+ self.assertEqual("2a1ace52255252df566af0faaedfbfa7", checkouts[0].token)
+ self.assertEqual(2, len(checkouts[0].line_items))
diff --git a/test/collection_listing_test.py b/test/collection_listing_test.py
new file mode 100644
index 00000000..4bea7fac
--- /dev/null
+++ b/test/collection_listing_test.py
@@ -0,0 +1,44 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class CollectionListingTest(TestCase):
+ def test_get_collection_listings(self):
+ self.fake("collection_listings", method="GET", code=200, body=self.load_fixture("collection_listings"))
+
+ collection_listings = shopify.CollectionListing.find()
+ self.assertEqual(1, len(collection_listings))
+ self.assertEqual(1, collection_listings[0].collection_id)
+ self.assertEqual("Home page", collection_listings[0].title)
+
+ def test_get_collection_listing(self):
+ self.fake("collection_listings/1", method="GET", code=200, body=self.load_fixture("collection_listing"))
+
+ collection_listing = shopify.CollectionListing.find(1)
+
+ self.assertEqual(1, collection_listing.collection_id)
+ self.assertEqual("Home page", collection_listing.title)
+
+ def test_reload_collection_listing(self):
+ self.fake("collection_listings/1", method="GET", code=200, body=self.load_fixture("collection_listing"))
+
+ collection_listing = shopify.CollectionListing()
+ collection_listing.collection_id = 1
+ collection_listing.reload()
+
+ self.assertEqual(1, collection_listing.collection_id)
+ self.assertEqual("Home page", collection_listing.title)
+
+ def test_get_collection_listing_product_ids(self):
+ self.fake(
+ "collection_listings/1/product_ids",
+ method="GET",
+ code=200,
+ body=self.load_fixture("collection_listing_product_ids"),
+ )
+
+ collection_listing = shopify.CollectionListing()
+ collection_listing.id = 1
+ product_ids = collection_listing.product_ids()
+
+ self.assertEqual([1, 2], product_ids)
diff --git a/test/collection_publication_test.py b/test/collection_publication_test.py
new file mode 100644
index 00000000..4bd6dc7d
--- /dev/null
+++ b/test/collection_publication_test.py
@@ -0,0 +1,70 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class CollectionPublicationTest(TestCase):
+ def test_find_all_collection_publications(self):
+ self.fake(
+ "publications/55650051/collection_publications",
+ method="GET",
+ body=self.load_fixture("collection_publications"),
+ )
+ collection_publications = shopify.CollectionPublication.find(publication_id=55650051)
+
+ self.assertEqual(96062799894, collection_publications[0].id)
+ self.assertEqual(60941828118, collection_publications[0].collection_id)
+
+ def test_find_collection_publication(self):
+ self.fake(
+ "publications/55650051/collection_publications/96062799894",
+ method="GET",
+ body=self.load_fixture("collection_publication"),
+ code=200,
+ )
+ collection_publication = shopify.CollectionPublication.find(96062799894, publication_id=55650051)
+
+ self.assertEqual(96062799894, collection_publication.id)
+ self.assertEqual(60941828118, collection_publication.collection_id)
+
+ def test_create_collection_publication(self):
+ self.fake(
+ "publications/55650051/collection_publications",
+ method="POST",
+ headers={"Content-type": "application/json"},
+ body=self.load_fixture("collection_publication"),
+ code=201,
+ )
+
+ collection_publication = shopify.CollectionPublication.create(
+ {
+ "publication_id": 55650051,
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "collection_id": 60941828118,
+ }
+ )
+
+ expected_body = {
+ "collection_publication": {
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "collection_id": 60941828118,
+ }
+ }
+
+ self.assertEqual(expected_body, json.loads(self.http.request.data.decode("utf-8")))
+
+ def test_destroy_collection_publication(self):
+ self.fake(
+ "publications/55650051/collection_publications/96062799894",
+ method="GET",
+ body=self.load_fixture("collection_publication"),
+ code=200,
+ )
+ collection_publication = shopify.CollectionPublication.find(96062799894, publication_id=55650051)
+
+ self.fake("publications/55650051/collection_publications/96062799894", method="DELETE", body="{}", code=200)
+ collection_publication.destroy()
+
+ self.assertEqual("DELETE", self.http.request.get_method())
diff --git a/test/currency_test.py b/test/currency_test.py
new file mode 100644
index 00000000..2d3b47c9
--- /dev/null
+++ b/test/currency_test.py
@@ -0,0 +1,22 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class CurrencyTest(TestCase):
+ def test_get_currencies(self):
+ self.fake("currencies", method="GET", code=200, body=self.load_fixture("currencies"))
+
+ currencies = shopify.Currency.find()
+ self.assertEqual(4, len(currencies))
+ self.assertEqual("AUD", currencies[0].currency)
+ self.assertEqual("2018-10-03T14:44:08-04:00", currencies[0].rate_updated_at)
+ self.assertEqual(True, currencies[0].enabled)
+ self.assertEqual("EUR", currencies[1].currency)
+ self.assertEqual("2018-10-03T14:44:08-04:00", currencies[1].rate_updated_at)
+ self.assertEqual(True, currencies[1].enabled)
+ self.assertEqual("GBP", currencies[2].currency)
+ self.assertEqual("2018-10-03T14:44:08-04:00", currencies[2].rate_updated_at)
+ self.assertEqual(True, currencies[2].enabled)
+ self.assertEqual("HKD", currencies[3].currency)
+ self.assertEqual("2018-10-03T14:44:08-04:00", currencies[3].rate_updated_at)
+ self.assertEqual(False, currencies[3].enabled)
diff --git a/test/customer_saved_search_test.py b/test/customer_saved_search_test.py
new file mode 100644
index 00000000..48873f15
--- /dev/null
+++ b/test/customer_saved_search_test.py
@@ -0,0 +1,29 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class CustomerSavedSearchTest(TestCase):
+ def setUp(self):
+ super(CustomerSavedSearchTest, self).setUp()
+ self.load_customer_saved_search()
+
+ def test_get_customers_from_customer_saved_search(self):
+ self.fake(
+ "customer_saved_searches/8899730/customers", body=self.load_fixture("customer_saved_search_customers")
+ )
+ self.assertEqual(1, len(self.customer_saved_search.customers()))
+ self.assertEqual(112223902, self.customer_saved_search.customers()[0].id)
+
+ def test_get_customers_from_customer_saved_search_with_params(self):
+ self.fake(
+ "customer_saved_searches/8899730/customers.json?limit=1",
+ extension=False,
+ body=self.load_fixture("customer_saved_search_customers"),
+ )
+ customers = self.customer_saved_search.customers(limit=1)
+ self.assertEqual(1, len(customers))
+ self.assertEqual(112223902, customers[0].id)
+
+ def load_customer_saved_search(self):
+ self.fake("customer_saved_searches/8899730", body=self.load_fixture("customer_saved_search"))
+ self.customer_saved_search = shopify.CustomerSavedSearch.find(8899730)
diff --git a/test/customer_test.py b/test/customer_test.py
new file mode 100644
index 00000000..04f2a0c8
--- /dev/null
+++ b/test/customer_test.py
@@ -0,0 +1,76 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class CustomerTest(TestCase):
+ def setUp(self):
+ super(CustomerTest, self).setUp()
+ self.fake("customers/207119551", method="GET", body=self.load_fixture("customer"))
+ self.customer = shopify.Customer.find(207119551)
+
+ def test_create_customer(self):
+ self.fake(
+ "customers", method="POST", body=self.load_fixture("customer"), headers={"Content-type": "application/json"}
+ )
+ customer = shopify.Customer()
+ customer.first_name = "Bob"
+ customer.last_name = "Lastnameson"
+ customer.email = "steve.lastnameson@example.com"
+ customer.verified_email = True
+ customer.password = "newpass"
+ customer.password_confirmation = "newpass"
+ self.assertEqual("newpass", customer.attributes["password"])
+ customer.save()
+ self.assertEqual("Bob", customer.first_name)
+ self.assertEqual("newpass", customer.attributes["password"])
+
+ def test_get_customer(self):
+ self.assertEqual("Bob", self.customer.first_name)
+
+ def test_search(self):
+ self.fake(
+ "customers/search.json?query=Bob+country%3AUnited+States",
+ extension=False,
+ body=self.load_fixture("customers_search"),
+ )
+
+ results = shopify.Customer.search(query="Bob country:United States")
+ self.assertEqual("Bob", results[0].first_name)
+
+ def test_send_invite_with_no_params(self):
+ customer_invite_fixture = self.load_fixture("customer_invite")
+ customer_invite = json.loads(customer_invite_fixture.decode("utf-8"))
+ self.fake(
+ "customers/207119551/send_invite",
+ method="POST",
+ body=customer_invite_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ customer_invite_response = self.customer.send_invite()
+ self.assertEqual(json.loads('{"customer_invite": {}}'), json.loads(self.http.request.data.decode("utf-8")))
+ self.assertIsInstance(customer_invite_response, shopify.CustomerInvite)
+ self.assertEqual(customer_invite["customer_invite"]["to"], customer_invite_response.to)
+
+ def test_send_invite_with_params(self):
+ customer_invite_fixture = self.load_fixture("customer_invite")
+ customer_invite = json.loads(customer_invite_fixture.decode("utf-8"))
+ self.fake(
+ "customers/207119551/send_invite",
+ method="POST",
+ body=customer_invite_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ customer_invite_response = self.customer.send_invite(shopify.CustomerInvite(customer_invite["customer_invite"]))
+ self.assertEqual(customer_invite, json.loads(self.http.request.data.decode("utf-8")))
+ self.assertIsInstance(customer_invite_response, shopify.CustomerInvite)
+ self.assertEqual(customer_invite["customer_invite"]["to"], customer_invite_response.to)
+
+ def test_get_customer_orders(self):
+ self.fake("customers/207119551", method="GET", body=self.load_fixture("customer"))
+ customer = shopify.Customer.find(207119551)
+ self.fake("customers/207119551/orders", method="GET", body=self.load_fixture("orders"))
+ orders = customer.orders()
+ self.assertIsInstance(orders[0], shopify.Order)
+ self.assertEqual(450789469, orders[0].id)
+ self.assertEqual(207119551, orders[0].customer.id)
diff --git a/test/discount_code_creation_test.py b/test/discount_code_creation_test.py
new file mode 100644
index 00000000..ce2e3156
--- /dev/null
+++ b/test/discount_code_creation_test.py
@@ -0,0 +1,17 @@
+from test.test_helper import TestCase
+import shopify
+
+
+class DiscountCodeCreationTest(TestCase):
+ def test_find_batch_job_discount_codes(self):
+ self.fake("price_rules/1213131", body=self.load_fixture("price_rule"))
+ price_rule = shopify.PriceRule.find(1213131)
+
+ self.fake("price_rules/1213131/batch/989355119", body=self.load_fixture("discount_code_creation"))
+ batch = price_rule.find_batch(989355119)
+
+ self.fake("price_rules/1213131/batch/989355119/discount_codes", body=self.load_fixture("batch_discount_codes"))
+ discount_codes = batch.discount_codes()
+
+ self.assertEqual("foo", discount_codes[0].code)
+ self.assertEqual("bar", discount_codes[2].code)
diff --git a/test/discount_code_test.py b/test/discount_code_test.py
new file mode 100644
index 00000000..44ffd55e
--- /dev/null
+++ b/test/discount_code_test.py
@@ -0,0 +1,31 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class DiscountCodeTest(TestCase):
+ def setUp(self):
+ super(DiscountCodeTest, self).setUp()
+ self.fake("price_rules/1213131/discount_codes/34", method="GET", body=self.load_fixture("discount_code"))
+ self.discount_code = shopify.DiscountCode.find(34, price_rule_id=1213131)
+
+ def test_find_a_specific_discount_code(self):
+ discount_code = shopify.DiscountCode.find(34, price_rule_id=1213131)
+ self.assertEqual("25OFF", discount_code.code)
+
+ def test_update_a_specific_discount_code(self):
+ self.discount_code.code = "BOGO"
+ self.fake(
+ "price_rules/1213131/discount_codes/34",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("discount_code"),
+ headers={"Content-type": "application/json"},
+ )
+ self.discount_code.save()
+ self.assertEqual("BOGO", json.loads(self.http.request.data.decode("utf-8"))["discount_code"]["code"])
+
+ def test_delete_a_specific_discount_code(self):
+ self.fake("price_rules/1213131/discount_codes/34", method="DELETE", body="destroyed")
+ self.discount_code.destroy()
+ self.assertEqual("DELETE", self.http.request.get_method())
diff --git a/test/disputes_test.py b/test/disputes_test.py
new file mode 100644
index 00000000..71fd01d0
--- /dev/null
+++ b/test/disputes_test.py
@@ -0,0 +1,16 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class DisputeTest(TestCase):
+ prefix = "/admin/api/unstable/shopify_payments"
+
+ def test_get_dispute(self):
+ self.fake("disputes", method="GET", prefix=self.prefix, body=self.load_fixture("disputes"))
+ disputes = shopify.Disputes.find()
+ self.assertGreater(len(disputes), 0)
+
+ def test_get_one_dispute(self):
+ self.fake("disputes/1052608616", method="GET", prefix=self.prefix, body=self.load_fixture("dispute"))
+ disputes = shopify.Disputes.find(1052608616)
+ self.assertEqual("won", disputes.status)
diff --git a/test/draft_order_test.py b/test/draft_order_test.py
new file mode 100644
index 00000000..12a359bd
--- /dev/null
+++ b/test/draft_order_test.py
@@ -0,0 +1,150 @@
+import shopify
+from test.test_helper import TestCase
+import json
+
+
+class DraftOrderTest(TestCase):
+ def setUp(self):
+ super(DraftOrderTest, self).setUp()
+ self.fake("draft_orders/517119332", body=self.load_fixture("draft_order"))
+ self.draft_order = shopify.DraftOrder.find(517119332)
+
+ def test_get_draft_order(self):
+ self.fake("draft_orders/517119332", method="GET", code=200, body=self.load_fixture("draft_order"))
+ draft_order = shopify.DraftOrder.find(517119332)
+ self.assertEqual(517119332, draft_order.id)
+
+ def test_get_all_draft_orders(self):
+ self.fake("draft_orders", method="GET", code=200, body=self.load_fixture("draft_orders"))
+ draft_orders = shopify.DraftOrder.find()
+ self.assertEqual(1, len(draft_orders))
+ self.assertEqual(517119332, draft_orders[0].id)
+
+ def test_get_count_draft_orders(self):
+ self.fake("draft_orders/count", method="GET", code=200, body='{"count": 16}')
+ draft_orders_count = shopify.DraftOrder.count()
+ self.assertEqual(16, draft_orders_count)
+
+ def test_create_draft_order(self):
+ self.fake(
+ "draft_orders",
+ method="POST",
+ code=201,
+ body=self.load_fixture("draft_order"),
+ headers={"Content-type": "application/json"},
+ )
+ draft_order = shopify.DraftOrder.create({"line_items": [{"quantity": 1, "variant_id": 39072856}]})
+ self.assertEqual(
+ json.loads('{"draft_order": {"line_items": [{"quantity": 1, "variant_id": 39072856}]}}'),
+ json.loads(self.http.request.data.decode("utf-8")),
+ )
+
+ def test_create_draft_order_202(self):
+ self.fake(
+ "draft_orders",
+ method="POST",
+ code=202,
+ body=self.load_fixture("draft_order"),
+ headers={"Content-type": "application/json"},
+ )
+ draft_order = shopify.DraftOrder.create({"line_items": [{"quantity": 1, "variant_id": 39072856}]})
+ self.assertEqual(39072856, draft_order.line_items[0].variant_id)
+
+ def test_update_draft_order(self):
+ self.draft_order.note = "Test new note"
+ self.fake(
+ "draft_orders/517119332",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("draft_order"),
+ headers={"Content-type": "application/json"},
+ )
+ self.draft_order.save()
+ self.assertEqual("Test new note", json.loads(self.http.request.data.decode("utf-8"))["draft_order"]["note"])
+
+ def test_send_invoice_with_no_params(self):
+ draft_order_invoice_fixture = self.load_fixture("draft_order_invoice")
+ draft_order_invoice = json.loads(draft_order_invoice_fixture.decode("utf-8"))
+ self.fake(
+ "draft_orders/517119332/send_invoice",
+ method="POST",
+ body=draft_order_invoice_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ draft_order_invoice_response = self.draft_order.send_invoice()
+ self.assertEqual(json.loads('{"draft_order_invoice": {}}'), json.loads(self.http.request.data.decode("utf-8")))
+ self.assertIsInstance(draft_order_invoice_response, shopify.DraftOrderInvoice)
+ self.assertEqual(draft_order_invoice["draft_order_invoice"]["to"], draft_order_invoice_response.to)
+
+ def test_send_invoice_with_params(self):
+ draft_order_invoice_fixture = self.load_fixture("draft_order_invoice")
+ draft_order_invoice = json.loads(draft_order_invoice_fixture.decode("utf-8"))
+ self.fake(
+ "draft_orders/517119332/send_invoice",
+ method="POST",
+ body=draft_order_invoice_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ draft_order_invoice_response = self.draft_order.send_invoice(
+ shopify.DraftOrderInvoice(draft_order_invoice["draft_order_invoice"])
+ )
+ self.assertEqual(draft_order_invoice, json.loads(self.http.request.data.decode("utf-8")))
+ self.assertIsInstance(draft_order_invoice_response, shopify.DraftOrderInvoice)
+ self.assertEqual(draft_order_invoice["draft_order_invoice"]["to"], draft_order_invoice_response.to)
+
+ def test_delete_draft_order(self):
+ self.fake("draft_orders/517119332", method="DELETE", body="destroyed")
+ self.draft_order.destroy()
+ self.assertEqual("DELETE", self.http.request.get_method())
+
+ def test_add_metafields_to_draft_order(self):
+ self.fake(
+ "draft_orders/517119332/metafields",
+ method="POST",
+ code=201,
+ body=self.load_fixture("metafield"),
+ headers={"Content-type": "application/json"},
+ )
+ field = self.draft_order.add_metafield(
+ shopify.Metafield(
+ {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"}
+ )
+ )
+ self.assertEqual(
+ json.loads(
+ '{"metafield":{"namespace":"contact","key":"email","value":"123@example.com","value_type":"string"}}'
+ ),
+ json.loads(self.http.request.data.decode("utf-8")),
+ )
+ self.assertFalse(field.is_new())
+ self.assertEqual("contact", field.namespace)
+ self.assertEqual("email", field.key)
+ self.assertEqual("123@example.com", field.value)
+
+ def test_get_metafields_for_draft_order(self):
+ self.fake("draft_orders/517119332/metafields", body=self.load_fixture("metafields"))
+ metafields = self.draft_order.metafields()
+ self.assertEqual(2, len(metafields))
+ self.assertIsInstance(metafields[0], shopify.Metafield)
+ self.assertIsInstance(metafields[1], shopify.Metafield)
+
+ def test_complete_draft_order_with_no_params(self):
+ completed_fixture = self.load_fixture("draft_order_completed")
+ completed_draft = json.loads(completed_fixture.decode("utf-8"))["draft_order"]
+ headers = {"Content-type": "application/json", "Content-length": "0"}
+ self.fake("draft_orders/517119332/complete", method="PUT", body=completed_fixture, headers=headers)
+ self.draft_order.complete()
+ self.assertEqual(completed_draft["status"], self.draft_order.status)
+ self.assertEqual(completed_draft["order_id"], self.draft_order.order_id)
+ self.assertIsNotNone(self.draft_order.completed_at)
+
+ def test_complete_draft_order_with_params(self):
+ completed_fixture = self.load_fixture("draft_order_completed")
+ completed_draft = json.loads(completed_fixture.decode("utf-8"))["draft_order"]
+ headers = {"Content-type": "application/json", "Content-length": "0"}
+ url = "draft_orders/517119332/complete.json?payment_pending=true"
+ self.fake(url, extension=False, method="PUT", body=completed_fixture, headers=headers)
+ self.draft_order.complete({"payment_pending": True})
+ self.assertEqual(completed_draft["status"], self.draft_order.status)
+ self.assertEqual(completed_draft["order_id"], self.draft_order.order_id)
+ self.assertIsNotNone(self.draft_order.completed_at)
diff --git a/test/event_test.py b/test/event_test.py
new file mode 100644
index 00000000..cbc28802
--- /dev/null
+++ b/test/event_test.py
@@ -0,0 +1,12 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class EventTest(TestCase):
+ def test_prefix_uses_resource(self):
+ prefix = shopify.Event._prefix(options={"resource": "orders", "resource_id": 42})
+ self.assertEqual("https://this-is-my-test-show.myshopify.com/admin/api/unstable/orders/42", prefix)
+
+ def test_prefix_doesnt_need_resource(self):
+ prefix = shopify.Event._prefix()
+ self.assertEqual("https://this-is-my-test-show.myshopify.com/admin/api/unstable", prefix)
diff --git a/test/fixtures/access_scopes.json b/test/fixtures/access_scopes.json
new file mode 100644
index 00000000..6f4fb0c2
--- /dev/null
+++ b/test/fixtures/access_scopes.json
@@ -0,0 +1,13 @@
+{
+ "access_scopes": [
+ {
+ "handle": "read_products"
+ },
+ {
+ "handle": "write_orders"
+ },
+ {
+ "handle": "read_orders"
+ }
+ ]
+}
diff --git a/test/fixtures/application_credit.json b/test/fixtures/application_credit.json
new file mode 100644
index 00000000..4d6c3de0
--- /dev/null
+++ b/test/fixtures/application_credit.json
@@ -0,0 +1,8 @@
+{
+ "application_credit": {
+ "id": 445365009,
+ "amount": "5.00",
+ "description": "credit for application refund",
+ "test": null
+ }
+}
diff --git a/test/fixtures/application_credits.json b/test/fixtures/application_credits.json
new file mode 100644
index 00000000..b487a523
--- /dev/null
+++ b/test/fixtures/application_credits.json
@@ -0,0 +1,10 @@
+{
+ "application_credits": [
+ {
+ "id": 445365009,
+ "amount": "5.00",
+ "description": "credit for application refund",
+ "test": null
+ }
+ ]
+}
diff --git a/test/fixtures/article.json b/test/fixtures/article.json
new file mode 100644
index 00000000..9910f0eb
--- /dev/null
+++ b/test/fixtures/article.json
@@ -0,0 +1,15 @@
+{
+ "article": {
+ "author": "Shopify",
+ "blog_id": 1008414260,
+ "body_html": null,
+ "created_at": "2012-07-06T13:57:28-04:00",
+ "id": 6242736,
+ "published_at": "2012-07-06T13:57:28-04:00",
+ "summary_html": null,
+ "title": "First Post",
+ "updated_at": "2012-07-06T13:57:51-04:00",
+ "user_id": null,
+ "tags": "consequuntur, cupiditate, repellendus"
+ }
+}
diff --git a/test/fixtures/articles.json b/test/fixtures/articles.json
new file mode 100644
index 00000000..5954b09f
--- /dev/null
+++ b/test/fixtures/articles.json
@@ -0,0 +1,39 @@
+{
+ "articles": [{
+ "author": "Shopify",
+ "blog_id": 1008414260,
+ "body_html": null,
+ "created_at": "2012-07-06T13:57:28-04:00",
+ "id": 6242736,
+ "published_at": "2012-07-06T13:57:28-04:00",
+ "summary_html": null,
+ "title": "First Post",
+ "updated_at": "2012-07-06T13:57:51-04:00",
+ "user_id": null,
+ "tags": "consequuntur, cupiditate, repellendus"
+ }, {
+ "author": "development shop",
+ "blog_id": 1008414260,
+ "body_html": null,
+ "created_at": "2013-04-21T18:10:35-04:00",
+ "id": 7739673,
+ "published_at": "2013-04-21T18:10:22-04:00",
+ "summary_html": null,
+ "title": "My second blog post",
+ "updated_at": "2013-04-21T18:10:35-04:00",
+ "user_id": 2221540,
+ "tags": ""
+ }, {
+ "author": "development shop",
+ "blog_id": 1008414260,
+ "body_html": null,
+ "created_at": "2013-04-21T18:11:19-04:00",
+ "id": 7739683,
+ "published_at": "2013-04-21T18:10:45-04:00",
+ "summary_html": null,
+ "title": "50% off sale",
+ "updated_at": "2013-04-21T18:11:19-04:00",
+ "user_id": 2221540,
+ "tags": ""
+ }]
+}
diff --git a/test/fixtures/asset.json b/test/fixtures/asset.json
new file mode 100644
index 00000000..fe29133a
--- /dev/null
+++ b/test/fixtures/asset.json
@@ -0,0 +1,9 @@
+{
+ "asset": {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "value": "\nFeatured Products
\n\n\n\n \n \t{% assign article = pages.frontpage %}\n\n
\n {% if article.content != \"\" %}\n\t\t
{{ article.title }}
\n
\n \t\t {{ article.content }}\n \t\t
\n \t{% else %}\n
\n \t In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
\n \t {{ \"Learn more about handles\" | link_to \"http://wiki.shopify.com/Handle\" }}\n
\n \t{% endif %}\n
\n\n
\n\n",
+ "key": "templates/index.liquid"
+ }
+}
diff --git a/test/fixtures/assets.json b/test/fixtures/assets.json
new file mode 100644
index 00000000..282ecafd
--- /dev/null
+++ b/test/fixtures/assets.json
@@ -0,0 +1,136 @@
+{
+ "assets": [
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-body-green.gif?1",
+ "key": "assets/bg-body-green.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-body-orange.gif?1",
+ "key": "assets/bg-body-orange.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-body-pink.gif?1",
+ "key": "assets/bg-body-pink.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-body.gif?1",
+ "key": "assets/bg-body.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-content.gif?1",
+ "key": "assets/bg-content.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-footer.gif?1",
+ "key": "assets/bg-footer.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-main.gif?1",
+ "key": "assets/bg-main.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/bg-sidebar.gif?1",
+ "key": "assets/bg-sidebar.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/shop.css?1",
+ "key": "assets/shop.css"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/shop.css.liquid?1",
+ "key": "assets/shop.css.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/shop.js?1",
+ "key": "assets/shop.js"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/sidebar-devider.gif?1",
+ "key": "assets/sidebar-devider.gif"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": "http://static.shopify.com/s/files/1/6909/3384/t/1/assets/sidebar-menu.jpg?1",
+ "key": "assets/sidebar-menu.jpg"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "config/settings.html"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "layout/theme.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "templates/article.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "templates/blog.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "templates/cart.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "templates/collection.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "templates/index.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "templates/page.liquid"
+ },
+ {
+ "created_at": "2010-07-12T15:31:50-04:00",
+ "updated_at": "2010-07-12T15:31:50-04:00",
+ "public_url": null,
+ "key": "templates/product.liquid"
+ }
+ ]
+}
diff --git a/test/fixtures/authors.json b/test/fixtures/authors.json
new file mode 100644
index 00000000..fc9db30e
--- /dev/null
+++ b/test/fixtures/authors.json
@@ -0,0 +1 @@
+{"authors": ["Shopify", "development shop", "development shop"]}
diff --git a/test/fixtures/balance.json b/test/fixtures/balance.json
new file mode 100644
index 00000000..851a2d76
--- /dev/null
+++ b/test/fixtures/balance.json
@@ -0,0 +1,8 @@
+{
+ "balance": [
+ {
+ "currency": "USD",
+ "amount": "53.99"
+ }
+ ]
+}
diff --git a/test/fixtures/batch_discount_codes.json b/test/fixtures/batch_discount_codes.json
new file mode 100644
index 00000000..f63d29db
--- /dev/null
+++ b/test/fixtures/batch_discount_codes.json
@@ -0,0 +1,19 @@
+{
+ "discount_codes": [
+ {
+ "id": null,
+ "code": "foo",
+ "errors": {}
+ },
+ {
+ "id": null,
+ "code": "",
+ "errors": {}
+ },
+ {
+ "id": null,
+ "code": "bar",
+ "errors": {}
+ }
+ ]
+}
diff --git a/test/fixtures/blog.json b/test/fixtures/blog.json
new file mode 100644
index 00000000..2c92f6d7
--- /dev/null
+++ b/test/fixtures/blog.json
@@ -0,0 +1,13 @@
+{
+ "blog": {
+ "handle": "test-blog",
+ "created_at": "2012-01-10T17:45:19-05:00",
+ "title": "Test Blog",
+ "template_suffix": null,
+ "updated_at": "2012-01-10T17:45:19-05:00",
+ "feedburner_location": null,
+ "id": 1008414260,
+ "feedburner": null,
+ "commentable": "no"
+ }
+}
diff --git a/test/fixtures/blogs.json b/test/fixtures/blogs.json
new file mode 100644
index 00000000..0749df28
--- /dev/null
+++ b/test/fixtures/blogs.json
@@ -0,0 +1,13 @@
+{
+ "blogs": [{
+ "handle": "test-blog",
+ "created_at": "2012-01-10T17:45:19-05:00",
+ "title": "Test Blog",
+ "template_suffix": null,
+ "updated_at": "2012-01-10T17:45:19-05:00",
+ "feedburner_location": null,
+ "id": 1008414260,
+ "feedburner": null,
+ "commentable": "no"
+ }]
+}
diff --git a/test/fixtures/carrier_service.json b/test/fixtures/carrier_service.json
new file mode 100644
index 00000000..b3f3b5f4
--- /dev/null
+++ b/test/fixtures/carrier_service.json
@@ -0,0 +1,9 @@
+{
+ "carrier_service": {
+ "name": "Some Postal Service",
+ "id": 123456,
+ "callback_url": "http://google.com",
+ "format": "json",
+ "service_discovery": true
+ }
+}
diff --git a/test/fixtures/carts.json b/test/fixtures/carts.json
new file mode 100644
index 00000000..4238684f
--- /dev/null
+++ b/test/fixtures/carts.json
@@ -0,0 +1,43 @@
+{
+ "carts": [
+ {
+ "id": 2,
+ "note": null,
+ "token": "3eed8183d4281db6ea82ee2b8f23e9cc",
+ "updated_at": "2012-02-13T14:39:37-05:00",
+ "line_items":
+ [
+ {
+ "id": 1,
+ "title": "test",
+ "price": "1.00",
+ "line_price": "1.00",
+ "quantity": 1,
+ "sku": "",
+ "grams": 1000,
+ "vendor": "test",
+ "variant_id": 1
+ }
+ ]
+ },
+ {
+ "id": 1,
+ "note": "",
+ "token": "49801807939c296be1e9a4bf6783a705",
+ "updated_at": "2012-02-13T14:39:12-05:00",
+ "line_items":[
+ {
+ "id": 1,
+ "title": "test",
+ "price": "1.00",
+ "line_price": "1.00",
+ "quantity": 1,
+ "sku": "",
+ "grams": 1000,
+ "vendor": "test",
+ "variant_id": 1
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/checkouts.json b/test/fixtures/checkouts.json
new file mode 100644
index 00000000..ce9bed94
--- /dev/null
+++ b/test/fixtures/checkouts.json
@@ -0,0 +1,186 @@
+{
+ "checkouts": [
+ {
+ "buyer_accepts_marketing": false,
+ "cart_token": "68778783ad298f1c80c3bafcddeea02f",
+ "closed_at": null,
+ "completed_at": null,
+ "created_at": "2012-10-12T07:05:27-04:00",
+ "currency": "USD",
+ "email": "bob.norman@hostmail.com",
+ "gateway": null,
+ "id": 450789469,
+ "landing_site": null,
+ "note": null,
+ "referring_site": null,
+ "shipping_lines": [
+ {
+ "title": "Free Shipping",
+ "price": "0.00",
+ "code": "Free Shipping",
+ "source": "shopify"
+ }
+ ],
+ "source": null,
+ "source_identifier": null,
+ "source_name": "web",
+ "source_url": null,
+ "subtotal_price": "398.00",
+ "taxes_included": false,
+ "token": "2a1ace52255252df566af0faaedfbfa7",
+ "total_discounts": "0.00",
+ "total_line_items_price": "398.00",
+ "total_price": "409.94",
+ "total_tax": "11.94",
+ "total_weight": 400,
+ "updated_at": "2012-10-12T07:05:27-04:00",
+ "line_items": [
+ {
+ "applied_discounts": [
+
+ ],
+ "compare_at_price": null,
+ "fulfillment_service": "manual",
+ "gift_card": false,
+ "grams": 200,
+ "id": 49148385,
+ "line_price": "199.00",
+ "price": "199.00",
+ "product_id": 632910392,
+ "properties": null,
+ "quantity": 1,
+ "requires_shipping": true,
+ "sku": "IPOD2008RED",
+ "tax_lines": [
+
+ ],
+ "taxable": true,
+ "title": "IPod Nano - 8GB",
+ "variant_id": 49148385,
+ "variant_title": "Red",
+ "vendor": "Apple"
+ },
+ {
+ "applied_discounts": [
+
+ ],
+ "compare_at_price": null,
+ "fulfillment_service": "manual",
+ "gift_card": false,
+ "grams": 200,
+ "id": 808950810,
+ "line_price": "199.00",
+ "price": "199.00",
+ "product_id": 632910392,
+ "properties": null,
+ "quantity": 1,
+ "requires_shipping": true,
+ "sku": "IPOD2008PINK",
+ "tax_lines": [
+
+ ],
+ "taxable": true,
+ "title": "IPod Nano - 8GB",
+ "variant_id": 808950810,
+ "variant_title": "Pink",
+ "vendor": "Apple"
+ }
+ ],
+ "name": "#450789469",
+ "note_attributes": [
+ {
+ "name": "custom engraving",
+ "value": "Happy Birthday"
+ },
+ {
+ "name": "colour",
+ "value": "green"
+ }
+ ],
+ "discount_codes": [
+ {
+ "code": "TENOFF",
+ "amount": "10.00"
+ }
+ ],
+ "abandoned_checkout_url": "https://checkout.local/orders/690933842/2a1ace52255252df566af0faaedfbfa7?recovered=1",
+ "tax_lines": [
+ {
+ "price": "11.94",
+ "rate": 0.06,
+ "title": "State Tax"
+ }
+ ],
+ "billing_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": "Bob",
+ "last_name": "Norman",
+ "latitude": "45.41634",
+ "longitude": "-75.6868",
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "shipping_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": "Bob",
+ "last_name": "Norman",
+ "latitude": "45.41634",
+ "longitude": "-75.6868",
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "customer": {
+ "accepts_marketing": false,
+ "created_at": "2014-03-07T16:12:37-05:00",
+ "email": "bob.norman@hostmail.com",
+ "first_name": "Bob",
+ "id": 207119551,
+ "last_name": "Norman",
+ "last_order_id": null,
+ "multipass_identifier": null,
+ "note": null,
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "updated_at": "2014-03-07T16:12:37-05:00",
+ "verified_email": true,
+ "tags": "",
+ "last_order_name": null,
+ "default_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": null,
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/collection_listing.json b/test/fixtures/collection_listing.json
new file mode 100644
index 00000000..00c72093
--- /dev/null
+++ b/test/fixtures/collection_listing.json
@@ -0,0 +1,11 @@
+{
+ "collection_id": 1,
+ "updated_at": "2017-01-09T13:59:09-05:00",
+ "body_html": null,
+ "default_product_image": null,
+ "handle": "frontpage",
+ "image": null,
+ "title": "Home page",
+ "sort_order": "alpha-asc",
+ "published_at": "2017-01-09T13:59:09-05:00"
+}
diff --git a/test/fixtures/collection_listing_product_ids.json b/test/fixtures/collection_listing_product_ids.json
new file mode 100644
index 00000000..192c2445
--- /dev/null
+++ b/test/fixtures/collection_listing_product_ids.json
@@ -0,0 +1,4 @@
+[
+ 1,
+ 2
+]
diff --git a/test/fixtures/collection_listings.json b/test/fixtures/collection_listings.json
new file mode 100644
index 00000000..be841820
--- /dev/null
+++ b/test/fixtures/collection_listings.json
@@ -0,0 +1,13 @@
+[
+ {
+ "collection_id": 1,
+ "updated_at": "2017-01-09T13:59:09-05:00",
+ "body_html": null,
+ "default_product_image": null,
+ "handle": "frontpage",
+ "image": null,
+ "title": "Home page",
+ "sort_order": "alpha-asc",
+ "published_at": "2017-01-09T13:59:09-05:00"
+ }
+]
diff --git a/test/fixtures/collection_publication.json b/test/fixtures/collection_publication.json
new file mode 100644
index 00000000..e284ea53
--- /dev/null
+++ b/test/fixtures/collection_publication.json
@@ -0,0 +1,11 @@
+{
+ "collection_publication": {
+ "id": 96062799894,
+ "publication_id": 55650051,
+ "published_at": "2018-09-05T17:22:31-04:00",
+ "published": true,
+ "created_at": "2018-09-05T17:22:31-04:00",
+ "updated_at": "2018-09-14T14:31:19-04:00",
+ "collection_id": 60941828118
+ }
+}
diff --git a/test/fixtures/collection_publications.json b/test/fixtures/collection_publications.json
new file mode 100644
index 00000000..93ccec46
--- /dev/null
+++ b/test/fixtures/collection_publications.json
@@ -0,0 +1,13 @@
+{
+ "collection_publications": [
+ {
+ "id": 96062799894,
+ "publication_id": 55650051,
+ "published_at": "2018-09-05T17:22:31-04:00",
+ "published": true,
+ "created_at": "2018-09-05T17:22:31-04:00",
+ "updated_at": "2018-09-14T14:31:19-04:00",
+ "collection_id": 60941828118
+ }
+ ]
+}
diff --git a/test/fixtures/currencies.json b/test/fixtures/currencies.json
new file mode 100644
index 00000000..29da014e
--- /dev/null
+++ b/test/fixtures/currencies.json
@@ -0,0 +1,24 @@
+{
+ "currencies": [
+ {
+ "currency": "AUD",
+ "rate_updated_at": "2018-10-03T14:44:08-04:00",
+ "enabled": true
+ },
+ {
+ "currency": "EUR",
+ "rate_updated_at": "2018-10-03T14:44:08-04:00",
+ "enabled": true
+ },
+ {
+ "currency": "GBP",
+ "rate_updated_at": "2018-10-03T14:44:08-04:00",
+ "enabled": true
+ },
+ {
+ "currency": "HKD",
+ "rate_updated_at": "2018-10-03T14:44:08-04:00",
+ "enabled": false
+ }
+ ]
+}
diff --git a/test/fixtures/customer.json b/test/fixtures/customer.json
new file mode 100644
index 00000000..b9adf563
--- /dev/null
+++ b/test/fixtures/customer.json
@@ -0,0 +1,59 @@
+{
+ "customer": {
+ "accepts_marketing": false,
+ "created_at": "2015-05-27T18:11:24-04:00",
+ "email": "bob.norman@hostmail.com",
+ "first_name": "Bob",
+ "id": 207119551,
+ "last_name": "Norman",
+ "last_order_id": 450789469,
+ "multipass_identifier": null,
+ "note": null,
+ "orders_count": 1,
+ "state": "disabled",
+ "tax_exempt": false,
+ "total_spent": "41.94",
+ "updated_at": "2015-05-27T18:11:24-04:00",
+ "verified_email": true,
+ "tags": "",
+ "last_order_name": "#1001",
+ "default_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": "",
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ },
+ "addresses": [
+ {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": "",
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/customer_invite.json b/test/fixtures/customer_invite.json
new file mode 100644
index 00000000..4f8f947c
--- /dev/null
+++ b/test/fixtures/customer_invite.json
@@ -0,0 +1,9 @@
+{
+ "customer_invite": {
+ "to": "paul.norman@example.com",
+ "from": "steve@apple.com",
+ "subject": "Welcome to my new store!",
+ "custom_message": "This is a test custom message.",
+ "bcc": [ ]
+ }
+}
diff --git a/test/fixtures/customer_saved_search.json b/test/fixtures/customer_saved_search.json
new file mode 100644
index 00000000..c8b0cbaa
--- /dev/null
+++ b/test/fixtures/customer_saved_search.json
@@ -0,0 +1,9 @@
+{
+ "customer_saved_search": {
+ "created_at": "2013-01-21T13:26:12-05:00",
+ "id": 8899730,
+ "name": "Accepts Marketing",
+ "updated_at": "2013-01-21T13:26:12-05:00",
+ "query": "accepts_marketing:1"
+ }
+}
diff --git a/test/fixtures/customer_saved_search_customers.json b/test/fixtures/customer_saved_search_customers.json
new file mode 100644
index 00000000..ca07c424
--- /dev/null
+++ b/test/fixtures/customer_saved_search_customers.json
@@ -0,0 +1,60 @@
+{
+ "customers": [
+ {
+ "accepts_marketing": true,
+ "created_at": "2013-01-29T16:33:35-05:00",
+ "email": "john.smith@gmail.com",
+ "first_name": "John",
+ "id": 112223902,
+ "last_name": "Smith",
+ "last_order_id": null,
+ "multipass_identifier": null,
+ "note": "This is sample note",
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "updated_at": "2013-03-05T17:00:05-05:00",
+ "verified_email": true,
+ "tags": "Buyer, Canadian",
+ "last_order_name": null,
+ "default_address": {
+ "address1": "1234 Simple Road",
+ "address2": null,
+ "city": "Ottawa",
+ "company": null,
+ "country": "Canada",
+ "first_name": "John",
+ "id": 152583148,
+ "last_name": "Smith",
+ "phone": "555-555-5555",
+ "province": "Ontario",
+ "zip": "K2H 0A0",
+ "name": "John Smith",
+ "province_code": "ON",
+ "country_code": "CA",
+ "country_name": "Canada",
+ "default": true
+ },
+ "addresses": [
+ {
+ "address1": "1234 Simple Road",
+ "address2": null,
+ "city": "Ottawa",
+ "company": null,
+ "country": "Canada",
+ "first_name": "John",
+ "id": 152583148,
+ "last_name": "Smith",
+ "phone": "555-555-5555",
+ "province": "Ontario",
+ "zip": "K2H 0A0",
+ "name": "John Smith",
+ "province_code": "ON",
+ "country_code": "CA",
+ "country_name": "Canada",
+ "default": true
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/customers_search.json b/test/fixtures/customers_search.json
new file mode 100644
index 00000000..d2b15489
--- /dev/null
+++ b/test/fixtures/customers_search.json
@@ -0,0 +1,60 @@
+{
+ "customers": [
+ {
+ "accepts_marketing": false,
+ "created_at": "2014-01-20T17:25:18-05:00",
+ "email": "bob.norman@hostmail.com",
+ "first_name": "Bob",
+ "id": 207119551,
+ "last_name": "Norman",
+ "last_order_id": null,
+ "multipass_identifier": null,
+ "note": null,
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "updated_at": "2014-01-20T17:25:18-05:00",
+ "verified_email": true,
+ "tags": "",
+ "last_order_name": null,
+ "default_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": null,
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ },
+ "addresses": [
+ {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": null,
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/discount_code.json b/test/fixtures/discount_code.json
new file mode 100644
index 00000000..4f3da0f1
--- /dev/null
+++ b/test/fixtures/discount_code.json
@@ -0,0 +1,9 @@
+{
+ "discount_code": {
+ "code": "25OFF",
+ "id": 34,
+ "usage_count": 3,
+ "created_at": "2016-09-11T09:00:00-04:00",
+ "updated_at": "2016-09-11T09:30:00-04:00"
+ }
+}
diff --git a/test/fixtures/discount_code_creation.json b/test/fixtures/discount_code_creation.json
new file mode 100644
index 00000000..27f3a973
--- /dev/null
+++ b/test/fixtures/discount_code_creation.json
@@ -0,0 +1,14 @@
+{
+ "discount_code_creation": {
+ "id": 989355119,
+ "price_rule_id": 1213131,
+ "started_at": null,
+ "completed_at": null,
+ "created_at": "2018-07-05T13:04:29-04:00",
+ "updated_at": "2018-07-05T13:04:29-04:00",
+ "status": "queued",
+ "codes_count": 3,
+ "imported_count": 0,
+ "failed_count": 0
+ }
+}
diff --git a/test/fixtures/discount_codes.json b/test/fixtures/discount_codes.json
new file mode 100644
index 00000000..453e384c
--- /dev/null
+++ b/test/fixtures/discount_codes.json
@@ -0,0 +1,11 @@
+{
+ "discount_codes": [
+ {
+ "code": "25OFF",
+ "id": 34,
+ "usage_count": 3,
+ "created_at": "2016-09-11T09:00:00-04:00",
+ "updated_at": "2016-09-11T09:30:00-04:00"
+ }
+ ]
+}
diff --git a/test/fixtures/discount_disabled.json b/test/fixtures/discount_disabled.json
new file mode 100644
index 00000000..2a5e7167
--- /dev/null
+++ b/test/fixtures/discount_disabled.json
@@ -0,0 +1,17 @@
+{
+ "discount": {
+ "id": 992807812,
+ "code": "quidagis?",
+ "value": "9999999.00",
+ "ends_at": "2015-08-27T23:59:59-04:00",
+ "starts_at": "2015-08-23T00:00:00-04:00",
+ "status": "disabled",
+ "usage_limit": 20,
+ "minimum_order_amount": "0.00",
+ "applies_to_id": null,
+ "applies_once": false,
+ "discount_type": "shipping",
+ "applies_to_resource": null,
+ "times_used": 0
+ }
+}
diff --git a/test/fixtures/dispute.json b/test/fixtures/dispute.json
new file mode 100644
index 00000000..e88489a8
--- /dev/null
+++ b/test/fixtures/dispute.json
@@ -0,0 +1,16 @@
+{
+ "dispute": {
+ "id": 1052608616,
+ "order_id": null,
+ "type": "chargeback",
+ "amount": "100.00",
+ "currency": "USD",
+ "reason": "fraudulent",
+ "network_reason_code": "4827",
+ "status": "won",
+ "evidence_due_by": "2013-07-03T19:00:00-04:00",
+ "evidence_sent_on": "2013-07-04T07:00:00-04:00",
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ }
+}
diff --git a/test/fixtures/disputes.json b/test/fixtures/disputes.json
new file mode 100644
index 00000000..867c7519
--- /dev/null
+++ b/test/fixtures/disputes.json
@@ -0,0 +1,102 @@
+{
+ "disputes": [
+ {
+ "id": 1052608616,
+ "order_id": null,
+ "type": "chargeback",
+ "amount": "100.00",
+ "currency": "USD",
+ "reason": "fraudulent",
+ "network_reason_code": "4827",
+ "status": "won",
+ "evidence_due_by": "2013-07-03T19:00:00-04:00",
+ "evidence_sent_on": "2013-07-04T07:00:00-04:00",
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ },
+ {
+ "id": 815713555,
+ "order_id": 625362839,
+ "type": "chargeback",
+ "amount": "11.50",
+ "currency": "USD",
+ "reason": "credit_not_processed",
+ "network_reason_code": "4827",
+ "status": "needs_response",
+ "evidence_due_by": "2020-11-17T19:00:00-05:00",
+ "evidence_sent_on": null,
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ },
+ {
+ "id": 782360659,
+ "order_id": 625362839,
+ "type": "chargeback",
+ "amount": "11.50",
+ "currency": "USD",
+ "reason": "fraudulent",
+ "network_reason_code": "4827",
+ "status": "won",
+ "evidence_due_by": "2013-07-03T19:00:00-04:00",
+ "evidence_sent_on": "2013-07-04T07:00:00-04:00",
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ },
+ {
+ "id": 670893524,
+ "order_id": 625362839,
+ "type": "inquiry",
+ "amount": "11.50",
+ "currency": "USD",
+ "reason": "fraudulent",
+ "network_reason_code": "4827",
+ "status": "needs_response",
+ "evidence_due_by": "2020-11-17T19:00:00-05:00",
+ "evidence_sent_on": null,
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ },
+ {
+ "id": 598735659,
+ "order_id": 625362839,
+ "type": "chargeback",
+ "amount": "11.50",
+ "currency": "USD",
+ "reason": "fraudulent",
+ "network_reason_code": "4827",
+ "status": "needs_response",
+ "evidence_due_by": "2020-11-17T19:00:00-05:00",
+ "evidence_sent_on": null,
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ },
+ {
+ "id": 85190714,
+ "order_id": 625362839,
+ "type": "chargeback",
+ "amount": "11.50",
+ "currency": "USD",
+ "reason": "fraudulent",
+ "network_reason_code": "4827",
+ "status": "under_review",
+ "evidence_due_by": "2020-11-17T19:00:00-05:00",
+ "evidence_sent_on": "2020-11-04T19:00:00-05:00",
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ },
+ {
+ "id": 35982383,
+ "order_id": 625362839,
+ "type": "chargeback",
+ "amount": "11.50",
+ "currency": "USD",
+ "reason": "subscription_canceled",
+ "network_reason_code": "4827",
+ "status": "needs_response",
+ "evidence_due_by": "2020-11-17T19:00:00-05:00",
+ "evidence_sent_on": null,
+ "finalized_on": null,
+ "initiated_at": "2013-05-03T20:00:00-04:00"
+ }
+ ]
+}
diff --git a/test/fixtures/draft_order.json b/test/fixtures/draft_order.json
new file mode 100644
index 00000000..3da56b48
--- /dev/null
+++ b/test/fixtures/draft_order.json
@@ -0,0 +1,159 @@
+{
+ "draft_order": {
+ "id": 517119332,
+ "note": "This is a note",
+ "email": "montana_hilpert@example.com",
+ "taxes_included": false,
+ "currency": "CAD",
+ "subtotal_price": "1007.41",
+ "total_tax": "0.00",
+ "total_price": "1027.41",
+ "invoice_sent_at": null,
+ "created_at": "2017-02-02T13:14:38-05:00",
+ "updated_at": "2017-02-02T13:14:38-05:00",
+ "tax_exempt": false,
+ "completed_at": null,
+ "name": "#D1",
+ "status": "open",
+ "line_items": [
+ {
+ "variant_id": 39072856,
+ "product_id": 632910392,
+ "title": "IPod Nano - 8gb",
+ "variant_title": "green",
+ "sku": "IPOD2008GREEN",
+ "vendor": null,
+ "price": "199.00",
+ "grams": 200,
+ "quantity": 1,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "fulfillment_service": "manual",
+ "tax_lines": [],
+ "applied_discount": null,
+ "name": "IPod Nano - 8gb - green",
+ "properties": [
+ {
+ "name": "Custom Engraving",
+ "value": "Happy Birthday"
+ }
+ ],
+ "custom": false
+ },
+ {
+ "variant_id": null,
+ "product_id": null,
+ "title": "Custom Item",
+ "variant_title": null,
+ "sku": null,
+ "vendor": null,
+ "price": "494.14",
+ "grams": 0,
+ "quantity": 2,
+ "requires_shipping": false,
+ "taxable": false,
+ "gift_card": false,
+ "fulfillment_service": "manual",
+ "tax_lines": [],
+ "applied_discount": {
+ "description": "A percentage discount for a custom line item",
+ "value": "3.58",
+ "title": "Custom",
+ "amount": "35.38",
+ "value_type": "percentage"
+ },
+ "name": "Custom item",
+ "properties": [],
+ "custom": true
+ }
+ ],
+ "shipping_address": {
+ "first_name": "Jan",
+ "address1": "512 Ernestina Forks",
+ "phone": "(639) 372 1289",
+ "city": "Lakefurt",
+ "zip": "24093",
+ "province": "Virginia",
+ "country": "United States",
+ "last_name": "Fisher",
+ "address2": "Apt. 702",
+ "company": "Steuber and Sons",
+ "latitude": 45.416311,
+ "longitude": -75.68683,
+ "name": "Jan Fisher",
+ "country_code": "US",
+ "province_code": "VA"
+ },
+ "billing_address": {
+ "first_name": "Jan",
+ "address1": "512 Ernestina Forks",
+ "phone": "(639) 372 1289",
+ "city": "Lakefurt",
+ "zip": "24093",
+ "province": "Virginia",
+ "country": "United States",
+ "last_name": "Fisher",
+ "address2": "Apt. 702",
+ "company": "Steuber and Sons",
+ "latitude": 45.416311,
+ "longitude": -75.68683,
+ "name": "Jan Fisher",
+ "country_code": "US",
+ "province_code": "VA"
+ },
+ "invoice_url": "https://checkout.myshopify.io/1/invoices/8e72bdccd0ac51067b947ac68c6f3804",
+ "applied_discount": {
+ "description": "A discount on the entire order",
+ "value": "1.48",
+ "title": "Custom",
+ "amount": "1.48",
+ "value_type": "fixed_amount"
+ },
+ "order_id": null,
+ "shipping_line": {
+ "title": "Custom shipping",
+ "price": "20.00",
+ "custom": true,
+ "handle": null
+ },
+ "tax_lines": [],
+ "tags": "",
+ "customer": {
+ "accepts_marketing": false,
+ "created_at": "2014-03-07T16:14:08-05:00",
+ "email": "bob.norman@hostmail.com",
+ "first_name": "Bob",
+ "id": 207119551,
+ "last_name": "Norman",
+ "last_order_id": null,
+ "multipass_identifier": null,
+ "note": null,
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "updated_at": "2014-03-07T16:14:08-05:00",
+ "verified_email": true,
+ "tags": "",
+ "last_order_name": null,
+ "default_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": null,
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+}
diff --git a/test/fixtures/draft_order_completed.json b/test/fixtures/draft_order_completed.json
new file mode 100644
index 00000000..03bdae21
--- /dev/null
+++ b/test/fixtures/draft_order_completed.json
@@ -0,0 +1,159 @@
+{
+ "draft_order": {
+ "id": 517119332,
+ "note": "This is a note",
+ "email": "montana_hilpert@example.com",
+ "taxes_included": false,
+ "currency": "CAD",
+ "subtotal_price": "1007.41",
+ "total_tax": "0.00",
+ "total_price": "1027.41",
+ "invoice_sent_at": null,
+ "created_at": "2017-02-02T13:14:38-05:00",
+ "updated_at": "2017-02-02T13:14:38-05:00",
+ "tax_exempt": false,
+ "completed_at": "2017-02-02T14:15:16-17:00",
+ "name": "#D1",
+ "status": "completed",
+ "line_items": [
+ {
+ "variant_id": 39072856,
+ "product_id": 632910392,
+ "title": "IPod Nano - 8gb",
+ "variant_title": "green",
+ "sku": "IPOD2008GREEN",
+ "vendor": null,
+ "price": "199.00",
+ "grams": 200,
+ "quantity": 1,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "fulfillment_service": "manual",
+ "tax_lines": [],
+ "applied_discount": null,
+ "name": "IPod Nano - 8gb - green",
+ "properties": [
+ {
+ "name": "Custom Engraving",
+ "value": "Happy Birthday"
+ }
+ ],
+ "custom": false
+ },
+ {
+ "variant_id": null,
+ "product_id": null,
+ "title": "Custom Item",
+ "variant_title": null,
+ "sku": null,
+ "vendor": null,
+ "price": "494.14",
+ "grams": 0,
+ "quantity": 2,
+ "requires_shipping": false,
+ "taxable": false,
+ "gift_card": false,
+ "fulfillment_service": "manual",
+ "tax_lines": [],
+ "applied_discount": {
+ "description": "A percentage discount for a custom line item",
+ "value": "3.58",
+ "title": "Custom",
+ "amount": "35.38",
+ "value_type": "percentage"
+ },
+ "name": "Custom item",
+ "properties": [],
+ "custom": true
+ }
+ ],
+ "shipping_address": {
+ "first_name": "Jan",
+ "address1": "512 Ernestina Forks",
+ "phone": "(639) 372 1289",
+ "city": "Lakefurt",
+ "zip": "24093",
+ "province": "Virginia",
+ "country": "United States",
+ "last_name": "Fisher",
+ "address2": "Apt. 702",
+ "company": "Steuber and Sons",
+ "latitude": 45.416311,
+ "longitude": -75.68683,
+ "name": "Jan Fisher",
+ "country_code": "US",
+ "province_code": "VA"
+ },
+ "billing_address": {
+ "first_name": "Jan",
+ "address1": "512 Ernestina Forks",
+ "phone": "(639) 372 1289",
+ "city": "Lakefurt",
+ "zip": "24093",
+ "province": "Virginia",
+ "country": "United States",
+ "last_name": "Fisher",
+ "address2": "Apt. 702",
+ "company": "Steuber and Sons",
+ "latitude": 45.416311,
+ "longitude": -75.68683,
+ "name": "Jan Fisher",
+ "country_code": "US",
+ "province_code": "VA"
+ },
+ "invoice_url": "https://checkout.myshopify.io/1/invoices/8e72bdccd0ac51067b947ac68c6f3804",
+ "applied_discount": {
+ "description": "A discount on the entire order",
+ "value": "1.48",
+ "title": "Custom",
+ "amount": "1.48",
+ "value_type": "fixed_amount"
+ },
+ "order_id": 450789469,
+ "shipping_line": {
+ "title": "Custom shipping",
+ "price": "20.00",
+ "custom": true,
+ "handle": null
+ },
+ "tax_lines": [],
+ "tags": "",
+ "customer": {
+ "accepts_marketing": false,
+ "created_at": "2014-03-07T16:14:08-05:00",
+ "email": "bob.norman@hostmail.com",
+ "first_name": "Bob",
+ "id": 207119551,
+ "last_name": "Norman",
+ "last_order_id": null,
+ "multipass_identifier": null,
+ "note": null,
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "updated_at": "2014-03-07T16:14:08-05:00",
+ "verified_email": true,
+ "tags": "",
+ "last_order_name": null,
+ "default_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": null,
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+}
diff --git a/test/fixtures/draft_order_invoice.json b/test/fixtures/draft_order_invoice.json
new file mode 100644
index 00000000..b1d424fd
--- /dev/null
+++ b/test/fixtures/draft_order_invoice.json
@@ -0,0 +1,9 @@
+{
+ "draft_order_invoice": {
+ "to": "paul.norman@example.com",
+ "from": "steve@apple.com",
+ "subject": "Here is your new order invoice!",
+ "custom_message": "This is a test custom message.",
+ "bcc": [ ]
+ }
+}
diff --git a/test/fixtures/draft_orders.json b/test/fixtures/draft_orders.json
new file mode 100644
index 00000000..d7db236e
--- /dev/null
+++ b/test/fixtures/draft_orders.json
@@ -0,0 +1,161 @@
+{
+ "draft_orders": [
+ {
+ "id": 517119332,
+ "note": "This is a note",
+ "email": "montana_hilpert@example.com",
+ "taxes_included": false,
+ "currency": "CAD",
+ "subtotal_price": "1007.41",
+ "total_tax": "0.00",
+ "total_price": "1027.41",
+ "invoice_sent_at": null,
+ "created_at": "2017-02-02T13:14:38-05:00",
+ "updated_at": "2017-02-02T13:14:38-05:00",
+ "tax_exempt": false,
+ "completed_at": null,
+ "name": "#D1",
+ "status": "open",
+ "line_items": [
+ {
+ "variant_id": 39072856,
+ "product_id": 632910392,
+ "title": "IPod Nano - 8gb",
+ "variant_title": "green",
+ "sku": "IPOD2008GREEN",
+ "vendor": null,
+ "price": "199.00",
+ "grams": 200,
+ "quantity": 1,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "fulfillment_service": "manual",
+ "tax_lines": [],
+ "applied_discount": null,
+ "name": "IPod Nano - 8gb - green",
+ "properties": [
+ {
+ "name": "Custom Engraving",
+ "value": "Happy Birthday"
+ }
+ ],
+ "custom": false
+ },
+ {
+ "variant_id": null,
+ "product_id": null,
+ "title": "Custom Item",
+ "variant_title": null,
+ "sku": null,
+ "vendor": null,
+ "price": "494.14",
+ "grams": 0,
+ "quantity": 2,
+ "requires_shipping": false,
+ "taxable": false,
+ "gift_card": false,
+ "fulfillment_service": "manual",
+ "tax_lines": [],
+ "applied_discount": {
+ "description": "A percentage discount for a custom line item",
+ "value": "3.58",
+ "title": "Custom",
+ "amount": "35.38",
+ "value_type": "percentage"
+ },
+ "name": "Custom item",
+ "properties": [],
+ "custom": true
+ }
+ ],
+ "shipping_address": {
+ "first_name": "Jan",
+ "address1": "512 Ernestina Forks",
+ "phone": "(639) 372 1289",
+ "city": "Lakefurt",
+ "zip": "24093",
+ "province": "Virginia",
+ "country": "United States",
+ "last_name": "Fisher",
+ "address2": "Apt. 702",
+ "company": "Steuber and Sons",
+ "latitude": 45.416311,
+ "longitude": -75.68683,
+ "name": "Jan Fisher",
+ "country_code": "US",
+ "province_code": "VA"
+ },
+ "billing_address": {
+ "first_name": "Jan",
+ "address1": "512 Ernestina Forks",
+ "phone": "(639) 372 1289",
+ "city": "Lakefurt",
+ "zip": "24093",
+ "province": "Virginia",
+ "country": "United States",
+ "last_name": "Fisher",
+ "address2": "Apt. 702",
+ "company": "Steuber and Sons",
+ "latitude": 45.416311,
+ "longitude": -75.68683,
+ "name": "Jan Fisher",
+ "country_code": "US",
+ "province_code": "VA"
+ },
+ "invoice_url": "https://checkout.myshopify.io/1/invoices/8e72bdccd0ac51067b947ac68c6f3804",
+ "applied_discount": {
+ "description": "A discount on the entire order",
+ "value": "1.48",
+ "title": "Custom",
+ "amount": "1.48",
+ "value_type": "fixed_amount"
+ },
+ "order_id": null,
+ "shipping_line": {
+ "title": "Custom shipping",
+ "price": "20.00",
+ "custom": true,
+ "handle": null
+ },
+ "tax_lines": [],
+ "tags": "",
+ "customer": {
+ "accepts_marketing": false,
+ "created_at": "2014-03-07T16:14:08-05:00",
+ "email": "bob.norman@hostmail.com",
+ "first_name": "Bob",
+ "id": 207119551,
+ "last_name": "Norman",
+ "last_order_id": null,
+ "multipass_identifier": null,
+ "note": null,
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "updated_at": "2014-03-07T16:14:08-05:00",
+ "verified_email": true,
+ "tags": "",
+ "last_order_name": null,
+ "default_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": null,
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/engagement.json b/test/fixtures/engagement.json
new file mode 100644
index 00000000..4f0c9c4d
--- /dev/null
+++ b/test/fixtures/engagement.json
@@ -0,0 +1,15 @@
+{
+ "engagements": [
+ {
+ "occurred_on": "2017-04-20",
+ "impressions_count": null,
+ "views_count": null,
+ "clicks_count": 10,
+ "shares_count": null,
+ "favorites_count": null,
+ "comments_count": null,
+ "ad_spend": null,
+ "is_cumulative": true
+ }
+ ]
+}
diff --git a/test/fixtures/events.json b/test/fixtures/events.json
new file mode 100644
index 00000000..24561f50
--- /dev/null
+++ b/test/fixtures/events.json
@@ -0,0 +1,31 @@
+{
+ "events": [
+ {
+ "verb": "placed",
+ "created_at": "2008-01-10T11:00:00-05:00",
+ "body": null,
+ "subject_id": 450789469,
+ "id": 852065041,
+ "subject_type": "Order",
+ "message": "Order was placed"
+ },
+ {
+ "verb": "confirmed",
+ "created_at": "2008-01-10T11:00:00-05:00",
+ "body": null,
+ "subject_id": 450789469,
+ "id": 164748010,
+ "subject_type": "Order",
+ "message": "Received new order #1001 by Bob Norman"
+ },
+ {
+ "verb": "authorization_success",
+ "created_at": "2008-01-10T11:00:00-05:00",
+ "body": null,
+ "subject_id": 450789469,
+ "id": 103105390,
+ "subject_type": "Order",
+ "message": "The customer successfully authorized us to capture 210.94 USD"
+ }
+ ]
+}
diff --git a/test/fixtures/fulfillment.json b/test/fixtures/fulfillment.json
new file mode 100644
index 00000000..6d8bd9e8
--- /dev/null
+++ b/test/fixtures/fulfillment.json
@@ -0,0 +1,49 @@
+{
+ "fulfillment": {
+ "created_at": "2013-11-01T16:06:08-04:00",
+ "id": 255858046,
+ "order_id": 450789469,
+ "service": "manual",
+ "status": "pending",
+ "tracking_company": "null-company",
+ "updated_at": "2013-11-01T16:06:08-04:00",
+ "tracking_number": "1Z2345",
+ "tracking_numbers": [
+ "1Z2345"
+ ],
+ "tracking_url": "http://www.google.com/search?q=1Z2345",
+ "tracking_urls": [
+ "http://www.google.com/search?q=1Z2345"
+ ],
+ "receipt": {
+ "testcase": true,
+ "authorization": "123456"
+ },
+ "line_items": [
+ {
+ "fulfillment_service": "manual",
+ "fulfillment_status": null,
+ "grams": 200,
+ "id": 466157049,
+ "price": "199.00",
+ "product_id": 632910392,
+ "quantity": 1,
+ "requires_shipping": true,
+ "sku": "IPOD2008GREEN",
+ "title": "IPod Nano - 8gb",
+ "variant_id": 39072856,
+ "variant_title": "green",
+ "vendor": null,
+ "name": "IPod Nano - 8gb - green",
+ "variant_inventory_management": "shopify",
+ "properties": [
+ {
+ "name": "Custom Engraving",
+ "value": "Happy Birthday"
+ }
+ ],
+ "product_exists": true
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/fulfillment_event.json b/test/fixtures/fulfillment_event.json
new file mode 100644
index 00000000..7115a158
--- /dev/null
+++ b/test/fixtures/fulfillment_event.json
@@ -0,0 +1,22 @@
+{
+ "fulfillment_event": {
+ "id": 12584341209251,
+ "fulfillment_id": 2608403447971,
+ "status": "label_printed",
+ "message": null,
+ "happened_at": "2021-01-25T16:32:23-05:00",
+ "city": null,
+ "province": null,
+ "country": null,
+ "zip": null,
+ "address1": null,
+ "latitude": null,
+ "longitude": null,
+ "shop_id": 49144037539,
+ "created_at": "2021-01-25T16:32:23-05:00",
+ "updated_at": "2021-01-25T16:32:23-05:00",
+ "estimated_delivery_at": null,
+ "order_id": 2776493818019,
+ "admin_graphql_api_id": "gid://shopify/FulfillmentEvent/12584341209251"
+ }
+}
diff --git a/test/fixtures/fulfillment_service.json b/test/fixtures/fulfillment_service.json
new file mode 100644
index 00000000..ae6e9c10
--- /dev/null
+++ b/test/fixtures/fulfillment_service.json
@@ -0,0 +1,10 @@
+{
+ "fulfillment_service": {
+ "name": "SomeService",
+ "id": 123456,
+ "inventory_management": false,
+ "tracking_support": true,
+ "requires_shipping_method": false,
+ "format": "json"
+ }
+}
diff --git a/test/fixtures/gift_card.json b/test/fixtures/gift_card.json
new file mode 100644
index 00000000..a1cff105
--- /dev/null
+++ b/test/fixtures/gift_card.json
@@ -0,0 +1,20 @@
+{
+ "gift_card": {
+ "api_client_id": null,
+ "balance": "25.00",
+ "created_at": "2015-05-11T10:16:40+10:00",
+ "currency": "AUD",
+ "customer_id": null,
+ "disabled_at": null,
+ "expires_on": null,
+ "id": 4208208,
+ "initial_value": "25.00",
+ "line_item_id": null,
+ "note": "Gift card note.",
+ "template_suffix": null,
+ "updated_at": "2015-05-11T15:13:15+10:00",
+ "user_id": 123456,
+ "last_characters":"c293",
+ "order_id":null
+ }
+}
diff --git a/test/fixtures/gift_card_adjustment.json b/test/fixtures/gift_card_adjustment.json
new file mode 100644
index 00000000..299c028a
--- /dev/null
+++ b/test/fixtures/gift_card_adjustment.json
@@ -0,0 +1,17 @@
+{
+ "adjustment": {
+ "remote_transaction_url": null,
+ "user_id": 0,
+ "created_at": "2018-04-02T16:45:14-04:00",
+ "updated_at": "2018-04-02T16:45:14-04:00",
+ "number": 3,
+ "note": null,
+ "amount": "100.00",
+ "gift_card_id": 4208208,
+ "order_transaction_id": null,
+ "processed_at": "2018-04-02T16:45:14-04:00",
+ "api_client_id": 2349816,
+ "remote_transaction_ref": null,
+ "id": 1796440070
+ }
+}
diff --git a/test/fixtures/gift_card_disabled.json b/test/fixtures/gift_card_disabled.json
new file mode 100644
index 00000000..576ae593
--- /dev/null
+++ b/test/fixtures/gift_card_disabled.json
@@ -0,0 +1,20 @@
+{
+ "gift_card": {
+ "api_client_id": null,
+ "balance": "25.00",
+ "created_at": "2015-05-11T10:16:40+10:00",
+ "currency": "AUD",
+ "customer_id": null,
+ "disabled_at": "2015-05-11T10:16:40+10:00",
+ "expires_on": null,
+ "id": 4208208,
+ "initial_value": "25.00",
+ "line_item_id": null,
+ "note": "Gift card note.",
+ "template_suffix": null,
+ "updated_at": "2015-05-11T15:13:15+10:00",
+ "user_id": 123456,
+ "last_characters":"c293",
+ "order_id":null
+ }
+}
diff --git a/test/fixtures/gift_cards.json b/test/fixtures/gift_cards.json
new file mode 100644
index 00000000..1a103e69
--- /dev/null
+++ b/test/fixtures/gift_cards.json
@@ -0,0 +1,20 @@
+{
+ "gift_cards":[{
+ "api_client_id": null,
+ "balance": "25.00",
+ "created_at": "2015-05-11T10:16:40+10:00",
+ "currency": "AUD",
+ "customer_id": null,
+ "disabled_at": null,
+ "expires_on": null,
+ "id": 4208208,
+ "initial_value": "25.00",
+ "line_item_id": null,
+ "note": "Gift card note.",
+ "template_suffix": null,
+ "updated_at": "2015-05-11T15:13:15+10:00",
+ "user_id": 123456,
+ "last_characters":"c293",
+ "order_id":null
+ }]
+}
diff --git a/test/fixtures/gift_cards_search.json b/test/fixtures/gift_cards_search.json
new file mode 100644
index 00000000..ebdc4adc
--- /dev/null
+++ b/test/fixtures/gift_cards_search.json
@@ -0,0 +1,20 @@
+{
+ "gift_cards":[{
+ "api_client_id": null,
+ "balance": "10.00",
+ "created_at": "2020-05-11T10:16:40+10:00",
+ "currency": "USD",
+ "customer_id": null,
+ "disabled_at": null,
+ "expires_on": null,
+ "id": 4208209,
+ "initial_value": "25.00",
+ "line_item_id": null,
+ "note": "balance10",
+ "template_suffix": null,
+ "updated_at": "2020-05-11T15:13:15+10:00",
+ "user_id": 123456,
+ "last_characters":"c294",
+ "order_id":null
+ }]
+}
diff --git a/test/fixtures/graphql.json b/test/fixtures/graphql.json
new file mode 100644
index 00000000..ee925166
--- /dev/null
+++ b/test/fixtures/graphql.json
@@ -0,0 +1,26 @@
+{
+ "shop": {
+ "name": "Apple Computers",
+ "city": "Cupertino",
+ "address1": "1 Infinite Loop",
+ "zip": "95014",
+ "created_at": "2007-12-31T19:00:00-05:00",
+ "shop_owner": "Steve Jobs",
+ "plan_name": "enterprise",
+ "public": false,
+ "country": "US",
+ "money_with_currency_format": "$ {{amount}} USD",
+ "money_format": "$ {{amount}}",
+ "domain": "shop.apple.com",
+ "taxes_included": null,
+ "id": 690933842,
+ "timezone": "(GMT-05:00) Eastern Time (US & Canada)",
+ "tax_shipping": null,
+ "phone": null,
+ "currency": "USD",
+ "myshopify_domain": "apple.myshopify.com",
+ "source": null,
+ "province": "CA",
+ "email": "steve@apple.com"
+ }
+}
diff --git a/test/fixtures/image.json b/test/fixtures/image.json
new file mode 100644
index 00000000..8da79add
--- /dev/null
+++ b/test/fixtures/image.json
@@ -0,0 +1,10 @@
+{
+ "image": {
+ "created_at": "2014-01-10T16:15:40-05:00",
+ "id": 850703190,
+ "position": 1,
+ "product_id": 632910392,
+ "updated_at": "2014-01-10T16:15:40-05:00",
+ "src": "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540"
+ }
+}
diff --git a/test/fixtures/image_metafields.json b/test/fixtures/image_metafields.json
new file mode 100644
index 00000000..b3a1594f
--- /dev/null
+++ b/test/fixtures/image_metafields.json
@@ -0,0 +1,16 @@
+{
+ "metafields": [
+ {
+ "created_at": "2014-08-17T19:06:50+09:00",
+ "description": null,
+ "id": 850703198,
+ "key": "alt",
+ "namespace": "tags",
+ "owner_id": 850703190,
+ "updated_at": "2014-08-27T00:30:17+09:00",
+ "value": "Image Alt Tag",
+ "value_type": "string",
+ "owner_resource": "product_image"
+ }
+ ]
+}
diff --git a/test/fixtures/images.json b/test/fixtures/images.json
new file mode 100644
index 00000000..57ff16c8
--- /dev/null
+++ b/test/fixtures/images.json
@@ -0,0 +1,20 @@
+{
+ "images": [
+ {
+ "created_at": "2014-01-17T16:11:52-05:00",
+ "id": 850703190,
+ "position": 1,
+ "product_id": 632910392,
+ "updated_at": "2014-01-17T16:11:52-05:00",
+ "src": "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389993112"
+ },
+ {
+ "created_at": "2014-01-17T16:11:52-05:00",
+ "id": 562641783,
+ "position": 2,
+ "product_id": 632910392,
+ "updated_at": "2014-01-17T16:11:52-05:00",
+ "src": "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano-2.png?v=1389993112"
+ }
+ ]
+}
diff --git a/test/fixtures/inventory_item.json b/test/fixtures/inventory_item.json
new file mode 100644
index 00000000..97235987
--- /dev/null
+++ b/test/fixtures/inventory_item.json
@@ -0,0 +1,9 @@
+{
+ "inventory_item": {
+ "id": 808950810,
+ "sku": "IPOD2008PINK",
+ "created_at": "2018-05-07T15:33:38-04:00",
+ "updated_at": "2018-05-07T15:33:38-04:00",
+ "tracked": true
+ }
+}
diff --git a/test/fixtures/inventory_items.json b/test/fixtures/inventory_items.json
new file mode 100644
index 00000000..01a69ec4
--- /dev/null
+++ b/test/fixtures/inventory_items.json
@@ -0,0 +1,25 @@
+{
+ "inventory_items": [
+ {
+ "id": 39072856,
+ "sku": "IPOD2008GREEN",
+ "created_at": "2018-05-07T15:33:38-04:00",
+ "updated_at": "2018-05-07T15:33:38-04:00",
+ "tracked": true
+ },
+ {
+ "id": 457924702,
+ "sku": "IPOD2008BLACK",
+ "created_at": "2018-05-07T15:33:38-04:00",
+ "updated_at": "2018-05-07T15:33:38-04:00",
+ "tracked": true
+ },
+ {
+ "id": 808950810,
+ "sku": "IPOD2008PINK",
+ "created_at": "2018-05-07T15:33:38-04:00",
+ "updated_at": "2018-05-07T15:33:38-04:00",
+ "tracked": true
+ }
+ ]
+}
diff --git a/test/fixtures/inventory_level.json b/test/fixtures/inventory_level.json
new file mode 100644
index 00000000..72bd930a
--- /dev/null
+++ b/test/fixtures/inventory_level.json
@@ -0,0 +1,8 @@
+{
+ "inventory_level": {
+ "inventory_item_id": 808950810,
+ "location_id": 905684977,
+ "available": 6,
+ "updated_at": "2018-05-07T15:51:26-04:00"
+ }
+}
diff --git a/test/fixtures/inventory_levels.json b/test/fixtures/inventory_levels.json
new file mode 100644
index 00000000..f238820c
--- /dev/null
+++ b/test/fixtures/inventory_levels.json
@@ -0,0 +1,28 @@
+{
+ "inventory_levels": [
+ {
+ "inventory_item_id": 39072856,
+ "location_id": 487838322,
+ "available": 27,
+ "updated_at": "2018-05-07T15:33:38-04:00"
+ },
+ {
+ "inventory_item_id": 808950810,
+ "location_id": 905684977,
+ "available": 1,
+ "updated_at": "2018-05-07T15:33:38-04:00"
+ },
+ {
+ "inventory_item_id": 808950810,
+ "location_id": 487838322,
+ "available": 9,
+ "updated_at": "2018-05-07T15:33:38-04:00"
+ },
+ {
+ "inventory_item_id": 39072856,
+ "location_id": 905684977,
+ "available": 3,
+ "updated_at": "2018-05-07T15:33:38-04:00"
+ }
+ ]
+}
diff --git a/test/fixtures/location.json b/test/fixtures/location.json
new file mode 100644
index 00000000..ae07fac4
--- /dev/null
+++ b/test/fixtures/location.json
@@ -0,0 +1,19 @@
+{
+ "location": {
+ "id": 487838322,
+ "name": "Fifth Avenue AppleStore",
+ "deleted_at": null,
+ "address1": null,
+ "address2": null,
+ "city": null,
+ "zip": null,
+ "province": null,
+ "country": "US",
+ "phone": null,
+ "created_at": "2015-12-08T11:44:58-05:00",
+ "updated_at": "2015-12-08T11:44:58-05:00",
+ "country_code": "US",
+ "country_name": "United States",
+ "province_code": null
+ }
+}
diff --git a/test/fixtures/location_inventory_levels.json b/test/fixtures/location_inventory_levels.json
new file mode 100644
index 00000000..85eb684a
--- /dev/null
+++ b/test/fixtures/location_inventory_levels.json
@@ -0,0 +1,16 @@
+{
+ "inventory_levels": [
+ {
+ "inventory_item_id": 39072856,
+ "location_id": 487838322,
+ "available": 27,
+ "updated_at": "2018-05-07T15:33:38-04:00"
+ },
+ {
+ "inventory_item_id": 808950810,
+ "location_id": 487838322,
+ "available": 9,
+ "updated_at": "2018-05-07T15:33:38-04:00"
+ }
+ ]
+}
diff --git a/test/fixtures/locations.json b/test/fixtures/locations.json
new file mode 100644
index 00000000..906f7b77
--- /dev/null
+++ b/test/fixtures/locations.json
@@ -0,0 +1,38 @@
+{
+ "locations": [
+ {
+ "id": 487838322,
+ "name": "Fifth Avenue AppleStore",
+ "deleted_at": null,
+ "address1": null,
+ "address2": null,
+ "city": null,
+ "zip": null,
+ "province": null,
+ "country": "US",
+ "phone": null,
+ "created_at": "2015-12-08T11:44:58-05:00",
+ "updated_at": "2015-12-08T11:44:58-05:00",
+ "country_code": "US",
+ "country_name": "United States",
+ "province_code": null
+ },
+ {
+ "id": 1034478814,
+ "name": "Berlin Store",
+ "deleted_at": null,
+ "address1": null,
+ "address2": null,
+ "city": null,
+ "zip": null,
+ "province": null,
+ "country": "DE",
+ "phone": null,
+ "created_at": "2015-12-08T11:44:58-05:00",
+ "updated_at": "2015-12-08T11:44:58-05:00",
+ "country_code": "DE",
+ "country_name": "Germany",
+ "province_code": null
+ }
+ ]
+}
diff --git a/test/fixtures/marketing_event.json b/test/fixtures/marketing_event.json
new file mode 100644
index 00000000..eedc2a5d
--- /dev/null
+++ b/test/fixtures/marketing_event.json
@@ -0,0 +1,28 @@
+{
+ "marketing_event": {
+ "id": 1,
+ "started_at": "2011-12-31T19:00:00-05:00",
+ "ended_at": null,
+ "event_target": "facebook",
+ "event_type": "post",
+ "scheduled_to_end_at": null,
+ "budget": "10.11",
+ "budget_type": "daily",
+ "currency": "GBP",
+ "utm_campaign": "1234567890",
+ "utm_source": "facebook",
+ "utm_medium": "facebook-post",
+ "utm_content": null,
+ "utm_term": null,
+ "manage_url": null,
+ "preview_url": null,
+ "description": null,
+ "marketing_channel": "social",
+ "paid": false,
+ "referring_domain": "facebook.com",
+ "breadcrumb_id": null,
+ "marketed_resources": [
+ { "type": "product", "id": 1 }
+ ]
+ }
+}
diff --git a/test/fixtures/marketing_events.json b/test/fixtures/marketing_events.json
new file mode 100644
index 00000000..f99778af
--- /dev/null
+++ b/test/fixtures/marketing_events.json
@@ -0,0 +1,54 @@
+{
+ "marketing_event": [{
+ "id": 1,
+ "started_at": "2011-12-31T19:00:00-05:00",
+ "ended_at": null,
+ "event_target": "facebook",
+ "event_type": "post",
+ "scheduled_to_end_at": null,
+ "budget": "10.11",
+ "budget_type": "daily",
+ "currency": "GBP",
+ "utm_campaign": "1234567890",
+ "utm_source": "facebook",
+ "utm_medium": "facebook-post",
+ "utm_content": null,
+ "utm_term": null,
+ "manage_url": null,
+ "preview_url": null,
+ "description": null,
+ "marketing_channel": "social",
+ "paid": false,
+ "referring_domain": "facebook.com",
+ "breadcrumb_id": null,
+ "marketed_resources": [
+ { "type": "product", "id": 1 }
+ ]
+ },
+ {
+ "id": 2,
+ "started_at": "2011-12-31T19:00:00-05:00",
+ "ended_at": null,
+ "event_target": "facebook",
+ "event_type": "post",
+ "scheduled_to_end_at": null,
+ "budget": "10.11",
+ "budget_type": "daily",
+ "currency": "USD",
+ "utm_campaign": "1234567891",
+ "utm_source": "facebook",
+ "utm_medium": "facebook-post",
+ "utm_content": null,
+ "utm_term": null,
+ "manage_url": null,
+ "preview_url": null,
+ "description": null,
+ "marketing_channel": "social",
+ "paid": false,
+ "referring_domain": "facebook.com",
+ "breadcrumb_id": null,
+ "marketed_resources": [
+ { "type": "product", "id": 2 }
+ ]
+ }]
+}
diff --git a/test/fixtures/metafield.json b/test/fixtures/metafield.json
new file mode 100644
index 00000000..b096d9df
--- /dev/null
+++ b/test/fixtures/metafield.json
@@ -0,0 +1,12 @@
+{
+ "metafields": {
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "namespace": "contact",
+ "id": 721389482,
+ "value": "123@example.com",
+ "description": null,
+ "key": "email",
+ "value_type": "string"
+ }
+}
diff --git a/test/fixtures/metafields.json b/test/fixtures/metafields.json
new file mode 100644
index 00000000..78995109
--- /dev/null
+++ b/test/fixtures/metafields.json
@@ -0,0 +1,24 @@
+{
+ "metafields": [
+ {
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "namespace": "affiliates",
+ "id": 721389482,
+ "value": "app_key",
+ "description": null,
+ "key": "app_key",
+ "value_type": "string"
+ },
+ {
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "namespace": "contact",
+ "id": 721389480,
+ "value": "1231231231",
+ "description": null,
+ "key": "phone",
+ "value_type": "string"
+ }
+ ]
+}
diff --git a/test/fixtures/metafields_count.json b/test/fixtures/metafields_count.json
new file mode 100644
index 00000000..a113c32f
--- /dev/null
+++ b/test/fixtures/metafields_count.json
@@ -0,0 +1 @@
+{"count":2}
diff --git a/test/fixtures/order.json b/test/fixtures/order.json
new file mode 100644
index 00000000..967b5498
--- /dev/null
+++ b/test/fixtures/order.json
@@ -0,0 +1,275 @@
+{
+ "order": {
+ "buyer_accepts_marketing": false,
+ "cancel_reason": null,
+ "cancelled_at": null,
+ "cart_token": "68778783ad298f1c80c3bafcddeea02f",
+ "checkout_token": null,
+ "closed_at": null,
+ "confirmed": false,
+ "created_at": "2008-01-10T11:00:00-05:00",
+ "currency": "USD",
+ "email": "bob.norman@hostmail.com",
+ "financial_status": "authorized",
+ "fulfillment_status": null,
+ "gateway": "authorize_net",
+ "id": 450789469,
+ "landing_site": "http://www.example.com?source=abc",
+ "location_id": null,
+ "name": "#1001",
+ "note": null,
+ "number": 1,
+ "reference": "fhwdgads",
+ "referring_site": "http://www.otherexample.com",
+ "source": null,
+ "subtotal_price": "398.00",
+ "taxes_included": false,
+ "test": false,
+ "token": "b1946ac92492d2347c6235b4d2611184",
+ "total_discounts": "0.00",
+ "total_line_items_price": "398.00",
+ "total_price": "409.94",
+ "total_price_usd": "409.94",
+ "total_tax": "11.94",
+ "total_weight": 0,
+ "updated_at": "2008-01-10T11:00:00-05:00",
+ "user_id": null,
+ "browser_ip": null,
+ "landing_site_ref": "abc",
+ "order_number": 1001,
+ "discount_codes": [
+ {
+ "code": "TENOFF",
+ "amount": "10.00"
+ }
+ ],
+ "note_attributes": [
+ {
+ "name": "custom engraving",
+ "value": "Happy Birthday"
+ },
+ {
+ "name": "colour",
+ "value": "green"
+ }
+ ],
+ "processing_method": "direct",
+ "checkout_id": 450789469,
+ "source_name": "web",
+ "tax_lines": [
+ {
+ "price": "11.94",
+ "rate": 0.06,
+ "title": "State Tax"
+ }
+ ],
+ "line_items": [
+ {
+ "fulfillment_service": "manual",
+ "fulfillment_status": null,
+ "grams": 200,
+ "id": 466157049,
+ "price": "199.00",
+ "product_id": 632910392,
+ "quantity": 1,
+ "requires_shipping": true,
+ "sku": "IPOD2008GREEN",
+ "title": "IPod Nano - 8gb",
+ "variant_id": 39072856,
+ "variant_title": "green",
+ "vendor": null,
+ "name": "IPod Nano - 8gb - green",
+ "variant_inventory_management": "shopify",
+ "properties": [
+ {
+ "name": "Custom Engraving",
+ "value": "Happy Birthday"
+ }
+ ],
+ "product_exists": true
+ },
+ {
+ "fulfillment_service": "manual",
+ "fulfillment_status": null,
+ "grams": 200,
+ "id": 518995019,
+ "price": "199.00",
+ "product_id": 632910392,
+ "quantity": 1,
+ "requires_shipping": true,
+ "sku": "IPOD2008RED",
+ "title": "IPod Nano - 8gb",
+ "variant_id": 49148385,
+ "variant_title": "red",
+ "vendor": null,
+ "name": "IPod Nano - 8gb - red",
+ "variant_inventory_management": "shopify",
+ "properties": [
+
+ ],
+ "product_exists": true
+ },
+ {
+ "fulfillment_service": "manual",
+ "fulfillment_status": null,
+ "grams": 200,
+ "id": 703073504,
+ "price": "199.00",
+ "product_id": 632910392,
+ "quantity": 1,
+ "requires_shipping": true,
+ "sku": "IPOD2008BLACK",
+ "title": "IPod Nano - 8gb",
+ "variant_id": 457924702,
+ "variant_title": "black",
+ "vendor": null,
+ "name": "IPod Nano - 8gb - black",
+ "variant_inventory_management": "shopify",
+ "properties": [
+
+ ],
+ "product_exists": true
+ }
+ ],
+ "shipping_lines": [
+ {
+ "code": "Free Shipping",
+ "price": "0.00",
+ "source": "shopify",
+ "title": "Free Shipping"
+ }
+ ],
+ "payment_details": {
+ "avs_result_code": null,
+ "credit_card_bin": null,
+ "cvv_result_code": null,
+ "credit_card_number": "XXXX-XXXX-XXXX-4242",
+ "credit_card_company": "Visa"
+ },
+ "billing_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": "Bob",
+ "last_name": "Norman",
+ "latitude": "45.41634",
+ "longitude": "-75.6868",
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "shipping_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": "Bob",
+ "last_name": "Norman",
+ "latitude": "45.41634",
+ "longitude": "-75.6868",
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "fulfillments": [
+ {
+ "created_at": "2014-01-22T15:58:27-05:00",
+ "id": 255858046,
+ "order_id": 450789469,
+ "service": "manual",
+ "status": "failure",
+ "tracking_company": null,
+ "updated_at": "2014-01-22T15:58:27-05:00",
+ "tracking_number": "1Z2345",
+ "tracking_numbers": [
+ "1Z2345"
+ ],
+ "tracking_url": "http://wwwapps.ups.com/etracking/tracking.cgi?InquiryNumber1=1Z2345&TypeOfInquiryNumber=T&AcceptUPSLicenseAgreement=yes&submit=Track",
+ "tracking_urls": [
+ "http://wwwapps.ups.com/etracking/tracking.cgi?InquiryNumber1=1Z2345&TypeOfInquiryNumber=T&AcceptUPSLicenseAgreement=yes&submit=Track"
+ ],
+ "receipt": {
+ "testcase": true,
+ "authorization": "123456"
+ },
+ "line_items": [
+ {
+ "fulfillment_service": "manual",
+ "fulfillment_status": null,
+ "grams": 200,
+ "id": 466157049,
+ "price": "199.00",
+ "product_id": 632910392,
+ "quantity": 1,
+ "requires_shipping": true,
+ "sku": "IPOD2008GREEN",
+ "title": "IPod Nano - 8gb",
+ "variant_id": 39072856,
+ "variant_title": "green",
+ "vendor": null,
+ "name": "IPod Nano - 8gb - green",
+ "variant_inventory_management": "shopify",
+ "properties": [
+ {
+ "name": "Custom Engraving",
+ "value": "Happy Birthday"
+ }
+ ],
+ "product_exists": true
+ }
+ ]
+ }
+ ],
+ "client_details": {
+ "accept_language": null,
+ "browser_ip": "0.0.0.0",
+ "session_hash": null,
+ "user_agent": null
+ },
+ "customer": {
+ "accepts_marketing": false,
+ "created_at": "2014-01-22T15:58:27-05:00",
+ "email": "bob.norman@hostmail.com",
+ "first_name": "Bob",
+ "id": 207119551,
+ "last_name": "Norman",
+ "last_order_id": null,
+ "multipass_identifier": null,
+ "note": null,
+ "orders_count": 0,
+ "state": "disabled",
+ "total_spent": "0.00",
+ "updated_at": "2014-01-22T15:58:27-05:00",
+ "verified_email": true,
+ "tags": "",
+ "last_order_name": null,
+ "default_address": {
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "company": null,
+ "country": "United States",
+ "first_name": null,
+ "id": 207119551,
+ "last_name": null,
+ "phone": "555-625-1199",
+ "province": "Kentucky",
+ "zip": "40202",
+ "name": null,
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+}
diff --git a/test/fixtures/order_risk.json b/test/fixtures/order_risk.json
new file mode 100644
index 00000000..3e5118a3
--- /dev/null
+++ b/test/fixtures/order_risk.json
@@ -0,0 +1,14 @@
+{
+ "risk": {
+ "cause_cancel": true,
+ "checkout_id": null,
+ "display": true,
+ "id": 284138680,
+ "message": "This order was placed from a proxy IP",
+ "order_id": 450789469,
+ "recommendation": "cancel",
+ "score": "1.0",
+ "source": "External",
+ "merchant_message": "This order was placed from a proxy IP"
+ }
+}
diff --git a/test/fixtures/order_risks.json b/test/fixtures/order_risks.json
new file mode 100644
index 00000000..7ecd2dcd
--- /dev/null
+++ b/test/fixtures/order_risks.json
@@ -0,0 +1,28 @@
+{
+ "risks": [
+ {
+ "cause_cancel": true,
+ "checkout_id": null,
+ "display": true,
+ "id": 284138680,
+ "message": "This order was placed from a proxy IP",
+ "order_id": 450789469,
+ "recommendation": "cancel",
+ "score": "1.0",
+ "source": "External",
+ "merchant_message": "This order was placed from a proxy IP"
+ },
+ {
+ "cause_cancel": true,
+ "checkout_id": null,
+ "display": true,
+ "id": 432527878,
+ "message": "This order came from an anonymous proxy",
+ "order_id": 450789469,
+ "recommendation": "cancel",
+ "score": "1.0",
+ "source": "External",
+ "merchant_message": "This order came from an anonymous proxy"
+ }
+ ]
+}
diff --git a/test/fixtures/orders.json b/test/fixtures/orders.json
new file mode 100644
index 00000000..925a4a5d
--- /dev/null
+++ b/test/fixtures/orders.json
@@ -0,0 +1,206 @@
+{
+ "orders": [
+ {
+ "id": 450789469,
+ "email": "bob.norman@hostmail.com",
+ "closed_at": null,
+ "created_at": "2008-01-10T11:00:00-05:00",
+ "updated_at": "2008-01-10T11:00:00-05:00",
+ "number": 1,
+ "note": null,
+ "token": "b1946ac92492d2347c6235b4d2611184",
+ "gateway": "authorize_net",
+ "test": false,
+ "total_price": "598.94",
+ "subtotal_price": "597.00",
+ "total_weight": 0,
+ "total_tax": "11.94",
+ "taxes_included": false,
+ "currency": "USD",
+ "financial_status": "partially_refunded",
+ "confirmed": true,
+ "total_discounts": "10.00",
+ "total_line_items_price": "597.00",
+ "cart_token": "68778783ad298f1c80c3bafcddeea02f",
+ "buyer_accepts_marketing": false,
+ "name": "#1001",
+ "referring_site": "http://www.otherexample.com",
+ "landing_site": "http://www.example.com?source=abc",
+ "cancelled_at": null,
+ "cancel_reason": null,
+ "total_price_usd": "598.94",
+ "checkout_token": "bd5a8aa1ecd019dd3520ff791ee3a24c",
+ "reference": "fhwdgads",
+ "user_id": null,
+ "location_id": null,
+ "source_identifier": "fhwdgads",
+ "source_url": null,
+ "processed_at": "2008-01-10T11:00:00-05:00",
+ "device_id": null,
+ "phone": "+557734881234",
+ "customer_locale": null,
+ "app_id": null,
+ "browser_ip": "0.0.0.0",
+ "client_details": {
+ "accept_language": null,
+ "browser_height": null,
+ "browser_ip": "0.0.0.0",
+ "browser_width": null,
+ "session_hash": null,
+ "user_agent": null
+ },
+ "landing_site_ref": "abc",
+ "order_number": 1001,
+ "payment_details": {
+ "credit_card_number": "•••• •••• •••• 4242",
+ "credit_card_company": "Visa"
+ },
+ "payment_gateway_names": [
+ "bogus"
+ ],
+ "tags": "",
+ "contact_email": "bob.norman@hostmail.com",
+ "order_status_url": "https://apple.myshopify.com/690933842/orders/b1946ac92492d2347c6235b4d2611184/authenticate?key=ccde591a93123786bd8d257abd970200",
+ "presentment_currency": "USD",
+ "total_line_items_price_set": {
+ "shop_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ }
+ },
+ "total_discounts_set": {
+ "shop_money": {
+ "amount": "10.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "10.00",
+ "currency_code": "USD"
+ }
+ },
+ "total_shipping_price_set": {
+ "shop_money": {
+ "amount": "0.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "0.00",
+ "currency_code": "USD"
+ }
+ },
+ "subtotal_price_set": {
+ "shop_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ }
+ },
+ "total_price_set": {
+ "shop_money": {
+ "amount": "598.94",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "598.94",
+ "currency_code": "USD"
+ }
+ },
+ "total_tax_set": {
+ "shop_money": {
+ "amount": "11.94",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "11.94",
+ "currency_code": "USD"
+ }
+ },
+ "admin_graphql_api_id": "gid://shopify/Order/450789469",
+ "billing_address": {
+ "first_name": "Bob",
+ "address1": "Chestnut Street 92",
+ "phone": "555-625-1199",
+ "city": "Louisville",
+ "zip": "40202",
+ "province": "Kentucky",
+ "country": "United States",
+ "last_name": "Norman",
+ "address2": "",
+ "company": null,
+ "latitude": 45.41634,
+ "longitude": -75.6868,
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "shipping_address": {
+ "first_name": "Bob",
+ "address1": "Chestnut Street 92",
+ "phone": "555-625-1199",
+ "city": "Louisville",
+ "zip": "40202",
+ "province": "Kentucky",
+ "country": "United States",
+ "last_name": "Norman",
+ "address2": "",
+ "company": null,
+ "latitude": 45.41634,
+ "longitude": -75.6868,
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "customer": {
+ "id": 207119551,
+ "email": "bob.norman@hostmail.com",
+ "accepts_marketing": false,
+ "created_at": "2021-02-12T13:51:00-05:00",
+ "updated_at": "2021-02-12T13:51:00-05:00",
+ "first_name": "Bob",
+ "last_name": "Norman",
+ "orders_count": 1,
+ "state": "disabled",
+ "total_spent": "199.65",
+ "last_order_id": 450789469,
+ "note": null,
+ "verified_email": true,
+ "multipass_identifier": null,
+ "tax_exempt": false,
+ "phone": "+16136120707",
+ "tags": "",
+ "last_order_name": "#1001",
+ "currency": "USD",
+ "accepts_marketing_updated_at": "2005-06-12T11:57:11-04:00",
+ "marketing_opt_in_level": null,
+ "tax_exemptions": [],
+ "admin_graphql_api_id": "gid://shopify/Customer/207119551",
+ "default_address": {
+ "id": 207119551,
+ "customer_id": 207119551,
+ "first_name": null,
+ "last_name": null,
+ "company": null,
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "province": "Kentucky",
+ "country": "United States",
+ "zip": "40202",
+ "phone": "555-625-1199",
+ "name": "",
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/payout.json b/test/fixtures/payout.json
new file mode 100644
index 00000000..e11a8f47
--- /dev/null
+++ b/test/fixtures/payout.json
@@ -0,0 +1,21 @@
+{
+ "payout": {
+ "id": 623721858,
+ "status": "paid",
+ "date": "2019-11-12",
+ "currency": "USD",
+ "amount": "41.90",
+ "summary": {
+ "adjustments_fee_amount": "0.12",
+ "adjustments_gross_amount": "2.13",
+ "charges_fee_amount": "1.32",
+ "charges_gross_amount": "44.52",
+ "refunds_fee_amount": "-0.23",
+ "refunds_gross_amount": "-3.54",
+ "reserved_funds_fee_amount": "0.00",
+ "reserved_funds_gross_amount": "0.00",
+ "retried_payouts_fee_amount": "0.00",
+ "retried_payouts_gross_amount": "0.00"
+ }
+ }
+}
diff --git a/test/fixtures/payouts.json b/test/fixtures/payouts.json
new file mode 100644
index 00000000..26ad9b3a
--- /dev/null
+++ b/test/fixtures/payouts.json
@@ -0,0 +1,118 @@
+{
+ "payouts": [
+ {
+ "id": 854088011,
+ "status": "scheduled",
+ "date": "2019-11-01",
+ "currency": "USD",
+ "amount": "43.12",
+ "summary": {
+ "adjustments_fee_amount": "0.12",
+ "adjustments_gross_amount": "2.13",
+ "charges_fee_amount": "1.32",
+ "charges_gross_amount": "45.52",
+ "refunds_fee_amount": "-0.23",
+ "refunds_gross_amount": "-3.54",
+ "reserved_funds_fee_amount": "0.00",
+ "reserved_funds_gross_amount": "0.00",
+ "retried_payouts_fee_amount": "0.00",
+ "retried_payouts_gross_amount": "0.00"
+ }
+ },
+ {
+ "id": 512467833,
+ "status": "failed",
+ "date": "2019-11-01",
+ "currency": "USD",
+ "amount": "43.12",
+ "summary": {
+ "adjustments_fee_amount": "0.12",
+ "adjustments_gross_amount": "2.13",
+ "charges_fee_amount": "1.32",
+ "charges_gross_amount": "45.52",
+ "refunds_fee_amount": "-0.23",
+ "refunds_gross_amount": "-3.54",
+ "reserved_funds_fee_amount": "0.00",
+ "reserved_funds_gross_amount": "0.00",
+ "retried_payouts_fee_amount": "0.00",
+ "retried_payouts_gross_amount": "0.00"
+ }
+ },
+ {
+ "id": 39438702,
+ "status": "in_transit",
+ "date": "2019-11-01",
+ "currency": "USD",
+ "amount": "43.12",
+ "summary": {
+ "adjustments_fee_amount": "0.12",
+ "adjustments_gross_amount": "2.13",
+ "charges_fee_amount": "1.32",
+ "charges_gross_amount": "45.52",
+ "refunds_fee_amount": "-0.23",
+ "refunds_gross_amount": "-3.54",
+ "reserved_funds_fee_amount": "0.00",
+ "reserved_funds_gross_amount": "0.00",
+ "retried_payouts_fee_amount": "0.00",
+ "retried_payouts_gross_amount": "0.00"
+ }
+ },
+ {
+ "id": 710174591,
+ "status": "paid",
+ "date": "2019-12-12",
+ "currency": "USD",
+ "amount": "41.90",
+ "summary": {
+ "adjustments_fee_amount": "0.12",
+ "adjustments_gross_amount": "2.13",
+ "charges_fee_amount": "1.32",
+ "charges_gross_amount": "44.52",
+ "refunds_fee_amount": "-0.23",
+ "refunds_gross_amount": "-3.54",
+ "reserved_funds_fee_amount": "0.00",
+ "reserved_funds_gross_amount": "0.00",
+ "retried_payouts_fee_amount": "0.00",
+ "retried_payouts_gross_amount": "0.00"
+ }
+ },
+ {
+ "id": 974708905,
+ "status": "paid",
+ "date": "2019-11-13",
+ "currency": "CAD",
+ "amount": "51.69",
+ "summary": {
+ "adjustments_fee_amount": "0.12",
+ "adjustments_gross_amount": "2.13",
+ "charges_fee_amount": "6.46",
+ "charges_gross_amount": "58.15",
+ "refunds_fee_amount": "-0.23",
+ "refunds_gross_amount": "-3.54",
+ "reserved_funds_fee_amount": "0.00",
+ "reserved_funds_gross_amount": "0.00",
+ "retried_payouts_fee_amount": "0.00",
+ "retried_payouts_gross_amount": "0.00"
+ }
+ },
+ {
+ "id": 623721858,
+ "status": "paid",
+ "date": "2019-11-12",
+ "currency": "USD",
+ "amount": "41.90",
+ "summary": {
+ "adjustments_fee_amount": "0.12",
+ "adjustments_gross_amount": "2.13",
+ "charges_fee_amount": "1.32",
+ "charges_gross_amount": "44.52",
+ "refunds_fee_amount": "-0.23",
+ "refunds_gross_amount": "-3.54",
+ "reserved_funds_fee_amount": "0.00",
+ "reserved_funds_gross_amount": "0.00",
+ "retried_payouts_fee_amount": "0.00",
+ "retried_payouts_gross_amount": "0.00"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/payouts_transactions.json b/test/fixtures/payouts_transactions.json
new file mode 100644
index 00000000..74220886
--- /dev/null
+++ b/test/fixtures/payouts_transactions.json
@@ -0,0 +1,404 @@
+{
+ "transactions": [
+ {
+ "id": 699519475,
+ "type": "debit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-50.00",
+ "fee": "0.00",
+ "net": "-50.00",
+ "source_id": 460709370,
+ "source_type": "adjustment",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2020-11-05T19:52:08-05:00"
+ },
+ {
+ "id": 77412310,
+ "type": "credit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "50.00",
+ "fee": "0.00",
+ "net": "50.00",
+ "source_id": 374511569,
+ "source_type": "Payments::Balance::AdjustmentReversal",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2020-11-05T19:52:08-05:00"
+ },
+ {
+ "id": 1006917261,
+ "type": "refund",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-3.45",
+ "fee": "0.00",
+ "net": "-3.45",
+ "source_id": 1006917261,
+ "source_type": "Payments::Refund",
+ "source_order_id": 217130470,
+ "source_order_transaction_id": 1006917261,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 777128868,
+ "type": "refund",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-8.05",
+ "fee": "0.00",
+ "net": "-8.05",
+ "source_id": 777128868,
+ "source_type": "Payments::Refund",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 758509248,
+ "type": "adjustment",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-1.50",
+ "fee": "-0.25",
+ "net": "-1.75",
+ "source_id": 764194150,
+ "source_type": "charge",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 746296004,
+ "type": "charge",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "10.00",
+ "fee": "2.00",
+ "net": "8.00",
+ "source_id": 746296004,
+ "source_type": "charge",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 515523000,
+ "type": "charge",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "11.50",
+ "fee": "0.65",
+ "net": "10.85",
+ "source_id": 1006917261,
+ "source_type": "Payments::Refund",
+ "source_order_id": 217130470,
+ "source_order_transaction_id": 1006917261,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 482793472,
+ "type": "adjustment",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "0.45",
+ "fee": "0.00",
+ "net": "0.45",
+ "source_id": 204289877,
+ "source_type": "charge",
+ "source_order_id": 217130470,
+ "source_order_transaction_id": 567994517,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 382557793,
+ "type": "adjustment",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "0.20",
+ "fee": "0.00",
+ "net": "0.20",
+ "source_id": 204289877,
+ "source_type": "charge",
+ "source_order_id": 217130470,
+ "source_order_transaction_id": 567994517,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 201521674,
+ "type": "refund",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-2.00",
+ "fee": "0.00",
+ "net": "-2.00",
+ "source_id": 971443537,
+ "source_type": "charge",
+ "source_order_id": 625362839,
+ "source_order_transaction_id": 461790020,
+ "processed_at": "2020-11-04T19:52:08-05:00"
+ },
+ {
+ "id": 620327031,
+ "type": "charge",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "11.50",
+ "fee": "0.63",
+ "net": "10.87",
+ "source_id": 620327031,
+ "source_type": "charge",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 726130462,
+ "type": "dispute",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-11.50",
+ "fee": "15.00",
+ "net": "-26.50",
+ "source_id": 598735659,
+ "source_type": "Payments::Dispute",
+ "source_order_id": 625362839,
+ "source_order_transaction_id": 897736458,
+ "processed_at": "2020-10-25T20:52:08-04:00"
+ },
+ {
+ "id": 996672915,
+ "type": "debit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-100.00",
+ "fee": "0.00",
+ "net": "-100.00",
+ "source_id": 996672915,
+ "source_type": "Payments::Balance::AdjustmentReversal",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 843310825,
+ "type": "charge",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "11.50",
+ "fee": "0.63",
+ "net": "10.87",
+ "source_id": 843310825,
+ "source_type": "charge",
+ "source_order_id": 625362839,
+ "source_order_transaction_id": 897736458,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 841651232,
+ "type": "debit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-100.00",
+ "fee": "0.00",
+ "net": "-100.00",
+ "source_id": 841651232,
+ "source_type": "Payments::Balance::AdjustmentReversal",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 717600021,
+ "type": "credit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "100.00",
+ "fee": "0.00",
+ "net": "100.00",
+ "source_id": 717600021,
+ "source_type": "adjustment",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 427940661,
+ "type": "credit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "150.00",
+ "fee": "0.00",
+ "net": "150.00",
+ "source_id": 427940661,
+ "source_type": "Payments::Balance::AdjustmentReversal",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 400852343,
+ "type": "reserve",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-42.00",
+ "fee": "0.00",
+ "net": "-42.00",
+ "source_id": null,
+ "source_type": null,
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 381560291,
+ "type": "debit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-150.00",
+ "fee": "0.00",
+ "net": "-150.00",
+ "source_id": 381560291,
+ "source_type": "adjustment",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 357948134,
+ "type": "charge",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "10.00",
+ "fee": "0.46",
+ "net": "9.54",
+ "source_id": 971443537,
+ "source_type": "charge",
+ "source_order_id": 625362839,
+ "source_order_transaction_id": 461790020,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 250467535,
+ "type": "reserve",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "42.00",
+ "fee": "0.00",
+ "net": "42.00",
+ "source_id": null,
+ "source_type": null,
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 217609728,
+ "type": "charge",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "11.50",
+ "fee": "0.00",
+ "net": "11.50",
+ "source_id": 930299385,
+ "source_type": "charge",
+ "source_order_id": 625362839,
+ "source_order_transaction_id": 348327371,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 138130604,
+ "type": "credit",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "150.00",
+ "fee": "0.00",
+ "net": "150.00",
+ "source_id": 138130604,
+ "source_type": "Payments::Balance::AdjustmentReversal",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2016-08-04T18:07:57-04:00"
+ },
+ {
+ "id": 567994517,
+ "type": "charge",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "11.50",
+ "fee": "0.65",
+ "net": "10.85",
+ "source_id": 204289877,
+ "source_type": "charge",
+ "source_order_id": 217130470,
+ "source_order_transaction_id": 567994517,
+ "processed_at": "2014-01-21T13:05:38-05:00"
+ },
+ {
+ "id": 854848137,
+ "type": "payout",
+ "test": false,
+ "payout_id": 623721858,
+ "payout_status": "paid",
+ "currency": "USD",
+ "amount": "-41.90",
+ "fee": "0.00",
+ "net": "-41.90",
+ "source_id": 623721858,
+ "source_type": "payout",
+ "source_order_id": null,
+ "source_order_transaction_id": null,
+ "processed_at": "2012-11-11T19:00:00-05:00"
+ }
+ ]
+}
diff --git a/test/fixtures/price_rule.json b/test/fixtures/price_rule.json
new file mode 100644
index 00000000..dfeb626e
--- /dev/null
+++ b/test/fixtures/price_rule.json
@@ -0,0 +1,18 @@
+{
+ "price_rule": {
+ "id": 1213131,
+ "title": "BOGO",
+ "target_type": "line_item",
+ "target_selection": "all",
+ "allocation_method": "across",
+ "value_type": "percentage",
+ "value": -100,
+ "once_per_customer": true,
+ "usage_limit": null,
+ "customer_selection": "all",
+ "prerequisite_subtotal_range": null,
+ "prerequisite_shipping_price_range": null,
+ "starts_at": "2017-05-30T04:13:56Z",
+ "ends_at": null
+ }
+}
diff --git a/test/fixtures/price_rules.json b/test/fixtures/price_rules.json
new file mode 100644
index 00000000..aef51c8e
--- /dev/null
+++ b/test/fixtures/price_rules.json
@@ -0,0 +1,36 @@
+{
+ "price_rules": [
+ {
+ "id": 1213131,
+ "title": "BOGO",
+ "target_type": "line_item",
+ "target_selection": "all",
+ "allocation_method": "across",
+ "value_type": "percentage",
+ "value": -100,
+ "once_per_customer": true,
+ "usage_limit": null,
+ "customer_selection": "all",
+ "prerequisite_subtotal_range": null,
+ "prerequisite_shipping_price_range": null,
+ "starts_at": "2017-05-30T04:13:56Z",
+ "ends_at": null
+ },
+ {
+ "id": 1213132,
+ "title": "TENOFF",
+ "target_type": "line_item",
+ "target_selection": "all",
+ "allocation_method": "each",
+ "value_type": "percentage",
+ "value": -10,
+ "once_per_customer": true,
+ "usage_limit": null,
+ "customer_selection": "all",
+ "prerequisite_subtotal_range": null,
+ "prerequisite_shipping_price_range": null,
+ "starts_at": "2017-05-30T04:13:56Z",
+ "ends_at": null
+ }
+ ]
+}
diff --git a/test/fixtures/product.json b/test/fixtures/product.json
new file mode 100644
index 00000000..52dadedd
--- /dev/null
+++ b/test/fixtures/product.json
@@ -0,0 +1,116 @@
+{
+ "product": {
+ "product_type": "Cult Products",
+ "handle": "ipod-nano",
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "body_html": "It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.
",
+ "title": "IPod Nano - 8GB",
+ "template_suffix": null,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "id": 632910392,
+ "tags": "Emotive, Flash Memory, MP3, Music",
+ "images": [
+ {
+ "position": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "product_id": 632910392,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0",
+ "id": 850703190
+ }
+ ],
+ "variants": [
+ {
+ "position": 1,
+ "price": "199.00",
+ "product_id": 632910392,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Pink",
+ "inventory_quantity": 10,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 808950810,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008PINK",
+ "option1": "Pink",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ },
+ {
+ "position": 2,
+ "price": "199.00",
+ "product_id": 632910392,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Red",
+ "inventory_quantity": 20,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 49148385,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008RED",
+ "option1": "Red",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ },
+ {
+ "position": 3,
+ "price": "199.00",
+ "product_id": 632910392,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Green",
+ "inventory_quantity": 30,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 39072856,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008GREEN",
+ "option1": "Green",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ },
+ {
+ "position": 4,
+ "price": "199.00",
+ "product_id": 632910392,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Black",
+ "inventory_quantity": 40,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 457924702,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008BLACK",
+ "option1": "Black",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ }
+ ],
+ "vendor": "Apple",
+ "published_at": "2007-12-31T19:00:00-05:00",
+ "options": [
+ {
+ "name": "Title"
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/product_listing.json b/test/fixtures/product_listing.json
new file mode 100644
index 00000000..85b64324
--- /dev/null
+++ b/test/fixtures/product_listing.json
@@ -0,0 +1,86 @@
+{
+ "product_id": 2,
+ "created_at": "2017-01-06T14:52:56-05:00",
+ "updated_at": "2017-01-06T14:52:56-05:00",
+ "body_html": null,
+ "handle": "synergistic-silk-chair",
+ "product_type": "morph magnetic solutions",
+ "title": "Synergistic Silk Chair",
+ "vendor": "O'Hara, Fritsch and Hudson",
+ "available": true,
+ "tags": "",
+ "published_at": "2017-01-06T14:52:53-05:00",
+ "images": [
+
+ ],
+ "options": [
+ {
+ "id": 2,
+ "name": "Color or something",
+ "product_id": 2,
+ "position": 1
+ }
+ ],
+ "variants": [
+ {
+ "id": 3,
+ "title": "Aerodynamic Copper Clock",
+ "option_values": [
+ {
+ "option_id": 2,
+ "name": "Color or something",
+ "value": "Aerodynamic Copper Clock"
+ }
+ ],
+ "price": "179.99",
+ "formatted_price": "$179.99",
+ "compare_at_price": null,
+ "grams": 8400,
+ "requires_shipping": true,
+ "sku": "",
+ "barcode": null,
+ "taxable": true,
+ "position": 1,
+ "available": true,
+ "inventory_policy": "deny",
+ "inventory_quantity": 810,
+ "inventory_management": "shopify",
+ "fulfillment_service": "manual",
+ "weight": 8.4,
+ "weight_unit": "kg",
+ "image_id": null,
+ "created_at": "2017-01-04T17:07:47-05:00",
+ "updated_at": "2017-01-04T17:07:47-05:00"
+ },
+ {
+ "id": 4,
+ "title": "Awesome Concrete Knife",
+ "option_values": [
+ {
+ "option_id": 2,
+ "name": "Color or something",
+ "value": "Awesome Concrete Knife"
+ }
+ ],
+ "price": "179.99",
+ "formatted_price": "$179.99",
+ "compare_at_price": null,
+ "grams": 8400,
+ "requires_shipping": true,
+ "sku": "",
+ "barcode": null,
+ "taxable": true,
+ "position": 2,
+ "available": true,
+ "inventory_policy": "deny",
+ "inventory_quantity": 1,
+ "inventory_management": null,
+ "fulfillment_service": "manual",
+ "weight": 8.4,
+ "weight_unit": "kg",
+ "image_id": null,
+ "created_at": "2017-01-04T17:07:47-05:00",
+ "updated_at": "2017-01-04T17:07:47-05:00"
+ }
+ ]
+}
diff --git a/test/fixtures/product_listing_product_ids.json b/test/fixtures/product_listing_product_ids.json
new file mode 100644
index 00000000..208d98ed
--- /dev/null
+++ b/test/fixtures/product_listing_product_ids.json
@@ -0,0 +1,4 @@
+[
+ 2,
+ 1
+]
diff --git a/test/fixtures/product_listings.json b/test/fixtures/product_listings.json
new file mode 100644
index 00000000..f3961722
--- /dev/null
+++ b/test/fixtures/product_listings.json
@@ -0,0 +1,174 @@
+[
+ {
+ "product_id": 2,
+ "created_at": "2017-01-06T14:52:56-05:00",
+ "updated_at": "2017-01-06T14:52:56-05:00",
+ "body_html": null,
+ "handle": "synergistic-silk-chair",
+ "product_type": "morph magnetic solutions",
+ "title": "Synergistic Silk Chair",
+ "vendor": "O'Hara, Fritsch and Hudson",
+ "available": true,
+ "tags": "",
+ "published_at": "2017-01-06T14:52:53-05:00",
+ "images": [
+
+ ],
+ "options": [
+ {
+ "id": 2,
+ "name": "Color or something",
+ "product_id": 2,
+ "position": 1
+ }
+ ],
+ "variants": [
+ {
+ "id": 3,
+ "title": "Aerodynamic Copper Clock",
+ "option_values": [
+ {
+ "option_id": 2,
+ "name": "Color or something",
+ "value": "Aerodynamic Copper Clock"
+ }
+ ],
+ "price": "179.99",
+ "formatted_price": "$179.99",
+ "compare_at_price": null,
+ "grams": 8400,
+ "requires_shipping": true,
+ "sku": "",
+ "barcode": null,
+ "taxable": true,
+ "position": 1,
+ "available": true,
+ "inventory_policy": "deny",
+ "inventory_quantity": 810,
+ "inventory_management": "shopify",
+ "fulfillment_service": "manual",
+ "weight": 8.4,
+ "weight_unit": "kg",
+ "image_id": null,
+ "created_at": "2017-01-04T17:07:47-05:00",
+ "updated_at": "2017-01-04T17:07:47-05:00"
+ },
+ {
+ "id": 4,
+ "title": "Awesome Concrete Knife",
+ "option_values": [
+ {
+ "option_id": 2,
+ "name": "Color or something",
+ "value": "Awesome Concrete Knife"
+ }
+ ],
+ "price": "179.99",
+ "formatted_price": "$179.99",
+ "compare_at_price": null,
+ "grams": 8400,
+ "requires_shipping": true,
+ "sku": "",
+ "barcode": null,
+ "taxable": true,
+ "position": 2,
+ "available": true,
+ "inventory_policy": "deny",
+ "inventory_quantity": 1,
+ "inventory_management": null,
+ "fulfillment_service": "manual",
+ "weight": 8.4,
+ "weight_unit": "kg",
+ "image_id": null,
+ "created_at": "2017-01-04T17:07:47-05:00",
+ "updated_at": "2017-01-04T17:07:47-05:00"
+ }
+ ]
+ },
+ {
+ "product_id": 1,
+ "created_at": "2017-01-06T14:52:54-05:00",
+ "updated_at": "2017-01-06T14:52:54-05:00",
+ "body_html": null,
+ "handle": "rustic-copper-bottle",
+ "product_type": "maximize viral channels",
+ "title": "Rustic Copper Bottle",
+ "vendor": "Kuphal and Sons",
+ "available": true,
+ "tags": "",
+ "published_at": "2017-01-06T14:52:52-05:00",
+ "images": [
+
+ ],
+ "options": [
+ {
+ "id": 1,
+ "name": "Color or something",
+ "product_id": 1,
+ "position": 1
+ }
+ ],
+ "variants": [
+ {
+ "id": 1,
+ "title": "Awesome Bronze Hat",
+ "option_values": [
+ {
+ "option_id": 1,
+ "name": "Color or something",
+ "value": "Awesome Bronze Hat"
+ }
+ ],
+ "price": "111.99",
+ "formatted_price": "$111.99",
+ "compare_at_price": null,
+ "grams": 1800,
+ "requires_shipping": true,
+ "sku": "",
+ "barcode": null,
+ "taxable": true,
+ "position": 1,
+ "available": true,
+ "inventory_policy": "deny",
+ "inventory_quantity": 65,
+ "inventory_management": "shopify",
+ "fulfillment_service": "manual",
+ "weight": 1.8,
+ "weight_unit": "kg",
+ "image_id": null,
+ "created_at": "2017-01-04T17:07:07-05:00",
+ "updated_at": "2017-01-04T17:07:07-05:00"
+ },
+ {
+ "id": 2,
+ "title": "Rustic Marble Bottle",
+ "option_values": [
+ {
+ "option_id": 1,
+ "name": "Color or something",
+ "value": "Rustic Marble Bottle"
+ }
+ ],
+ "price": "111.99",
+ "formatted_price": "$111.99",
+ "compare_at_price": null,
+ "grams": 1800,
+ "requires_shipping": true,
+ "sku": "",
+ "barcode": null,
+ "taxable": true,
+ "position": 2,
+ "available": true,
+ "inventory_policy": "deny",
+ "inventory_quantity": 1,
+ "inventory_management": null,
+ "fulfillment_service": "manual",
+ "weight": 1.8,
+ "weight_unit": "kg",
+ "image_id": null,
+ "created_at": "2017-01-04T17:07:07-05:00",
+ "updated_at": "2017-01-04T17:07:07-05:00"
+ }
+ ]
+ }
+]
diff --git a/test/fixtures/product_publication.json b/test/fixtures/product_publication.json
new file mode 100644
index 00000000..593388c5
--- /dev/null
+++ b/test/fixtures/product_publication.json
@@ -0,0 +1,11 @@
+{
+ "product_publication": {
+ "id": 647162527768,
+ "publication_id": 55650051,
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": true,
+ "created_at": "2018-01-29T14:06:08-05:00",
+ "updated_at": "2018-09-26T15:39:05-04:00",
+ "product_id": 8267093571
+ }
+}
diff --git a/test/fixtures/product_publications.json b/test/fixtures/product_publications.json
new file mode 100644
index 00000000..761f61ae
--- /dev/null
+++ b/test/fixtures/product_publications.json
@@ -0,0 +1,13 @@
+{
+ "product_publications": [
+ {
+ "id": 647162527768,
+ "publication_id": 55650051,
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": true,
+ "created_at": "2018-01-29T14:06:08-05:00",
+ "updated_at": "2018-09-26T15:39:05-04:00",
+ "product_id": 8267093571
+ }
+ ]
+}
diff --git a/test/fixtures/products.json b/test/fixtures/products.json
new file mode 100644
index 00000000..4258cf6a
--- /dev/null
+++ b/test/fixtures/products.json
@@ -0,0 +1,206 @@
+[
+ {
+ "product_type": "Cult Products",
+ "handle": "ipod-nano",
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "body_html": "It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.
",
+ "title": "IPod Nano - 8GB",
+ "template_suffix": null,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "id": 1,
+ "tags": "Emotive, Flash Memory, MP3, Music",
+ "images": [
+ {
+ "position": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "product_id": 1,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0",
+ "id": 850703190
+ }
+ ],
+ "variants": [
+ {
+ "position": 1,
+ "price": "199.00",
+ "product_id": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Pink",
+ "inventory_quantity": 10,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 808950810,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008PINK",
+ "option1": "Pink",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ }
+ ],
+ "vendor": "Apple",
+ "published_at": "2007-12-31T19:00:00-05:00",
+ "options": [
+ {
+ "name": "Title"
+ }
+ ]
+ },
+ {
+ "product_type": "Cult Products",
+ "handle": "ipod-nano",
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "body_html": "It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.
",
+ "title": "IPod Nano - 8GB",
+ "template_suffix": null,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "id": 2,
+ "tags": "Emotive, Flash Memory, MP3, Music",
+ "images": [
+ {
+ "position": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "product_id": 2,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0",
+ "id": 850703190
+ }
+ ],
+ "variants": [
+ {
+ "position": 1,
+ "price": "199.00",
+ "product_id": 2,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Pink",
+ "inventory_quantity": 10,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 808950810,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008PINK",
+ "option1": "Pink",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ }
+ ],
+ "vendor": "Apple",
+ "published_at": "2007-12-31T19:00:00-05:00",
+ "options": [
+ {
+ "name": "Title"
+ }
+ ]
+ },
+ {
+ "product_type": "Cult Products",
+ "handle": "ipod-nano",
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "body_html": "It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.
",
+ "title": "IPod Nano - 8GB",
+ "template_suffix": null,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "id": 3,
+ "tags": "Emotive, Flash Memory, MP3, Music",
+ "images": [
+ {
+ "position": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "product_id": 2,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0",
+ "id": 850703190
+ }
+ ],
+ "variants": [
+ {
+ "position": 1,
+ "price": "199.00",
+ "product_id": 2,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Pink",
+ "inventory_quantity": 10,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 808950810,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008PINK",
+ "option1": "Pink",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ }
+ ],
+ "vendor": "Apple",
+ "published_at": "2007-12-31T19:00:00-05:00",
+ "options": [
+ {
+ "name": "Title"
+ }
+ ]
+ },
+ {
+ "product_type": "Cult Products",
+ "handle": "ipod-nano",
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "body_html": "It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.
",
+ "title": "IPod Nano - 8GB",
+ "template_suffix": null,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "id": 4,
+ "tags": "Emotive, Flash Memory, MP3, Music",
+ "images": [
+ {
+ "position": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "product_id": 4,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0",
+ "id": 850703190
+ }
+ ],
+ "variants": [
+ {
+ "position": 1,
+ "price": "199.00",
+ "product_id": 4,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "requires_shipping": true,
+ "title": "Pink",
+ "inventory_quantity": 10,
+ "compare_at_price": null,
+ "inventory_policy": "continue",
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_management": "shopify",
+ "id": 808950810,
+ "taxable": true,
+ "grams": 200,
+ "sku": "IPOD2008PINK",
+ "option1": "Pink",
+ "fulfillment_service": "manual",
+ "option2": null,
+ "option3": null
+ }
+ ],
+ "vendor": "Apple",
+ "published_at": "2007-12-31T19:00:00-05:00",
+ "options": [
+ {
+ "name": "Title"
+ }
+ ]
+ }
+]
diff --git a/test/fixtures/publications.json b/test/fixtures/publications.json
new file mode 100644
index 00000000..ab9e67fb
--- /dev/null
+++ b/test/fixtures/publications.json
@@ -0,0 +1,9 @@
+{
+ "publications": [
+ {
+ "id": 55650051,
+ "created_at": "2016-05-20T13:12:10-04:00",
+ "name": "Buy Button"
+ }
+ ]
+}
diff --git a/test/fixtures/recurring_application_charge_adjustment.json b/test/fixtures/recurring_application_charge_adjustment.json
new file mode 100644
index 00000000..6e4502d5
--- /dev/null
+++ b/test/fixtures/recurring_application_charge_adjustment.json
@@ -0,0 +1,5 @@
+{
+ "recurring_application_charge": {
+ "update_capped_amount_url": "http://apple.myshopify.com/admin/charges/654381177/confirm_update_capped_amount?signature=BAhpBHkQASc%3D--419fc7424f8c290ac2b21b9004ed223e35b52164"
+ }
+}
diff --git a/test/fixtures/recurring_application_charges.json b/test/fixtures/recurring_application_charges.json
new file mode 100644
index 00000000..c8588fa8
--- /dev/null
+++ b/test/fixtures/recurring_application_charges.json
@@ -0,0 +1,40 @@
+{
+ "recurring_application_charges": [
+ {
+ "activated_on": null,
+ "api_client_id": 755357713,
+ "billing_on": "2015-01-15T00:00:00+00:00",
+ "cancelled_on": null,
+ "created_at": "2015-01-16T11:45:44-05:00",
+ "id": 455696195,
+ "name": "Super Mega Plan",
+ "price": "15.00",
+ "return_url": "http://yourapp.com",
+ "status": "accepted",
+ "test": null,
+ "trial_days": 0,
+ "trial_ends_on": null,
+ "updated_at": "2015-01-16T11:46:56-05:00",
+ "decorated_return_url": "http://yourapp.com?charge_id=455696195"
+ },
+ {
+ "activated_on": "2015-01-15T00:00:00+00:00",
+ "api_client_id": 755357713,
+ "billing_on": "2015-01-15T00:00:00+00:00",
+ "cancelled_on": null,
+ "created_at": "2015-01-16T11:45:44-05:00",
+ "id": 455696195,
+ "name": "Super Mega Plan 2",
+ "price": "15.00",
+ "return_url": "http://yourapp.com",
+ "status": "active",
+ "test": null,
+ "trial_days": 0,
+ "trial_ends_on": null,
+ "updated_at": "2015-01-16T11:46:56-05:00",
+ "decorated_return_url": "http://yourapp.com?charge_id=455696195",
+ "capped_amount": 100,
+ "terms": "$1 for 1000 emails"
+ }
+ ]
+}
diff --git a/test/fixtures/recurring_application_charges_no_active.json b/test/fixtures/recurring_application_charges_no_active.json
new file mode 100644
index 00000000..f3812604
--- /dev/null
+++ b/test/fixtures/recurring_application_charges_no_active.json
@@ -0,0 +1,38 @@
+{
+ "recurring_application_charges": [
+ {
+ "activated_on": null,
+ "api_client_id": 755357713,
+ "billing_on": "2015-01-15T00:00:00+00:00",
+ "cancelled_on": null,
+ "created_at": "2015-01-16T11:45:44-05:00",
+ "id": 455696195,
+ "name": "Super Mega Plan",
+ "price": "15.00",
+ "return_url": "http://yourapp.com",
+ "status": "accepted",
+ "test": null,
+ "trial_days": 0,
+ "trial_ends_on": null,
+ "updated_at": "2015-01-16T11:46:56-05:00",
+ "decorated_return_url": "http://yourapp.com?charge_id=455696195"
+ },
+ {
+ "activated_on": "2015-01-15T00:00:00+00:00",
+ "api_client_id": 755357713,
+ "billing_on": "2015-01-15T00:00:00+00:00",
+ "cancelled_on": null,
+ "created_at": "2015-01-16T11:45:44-05:00",
+ "id": 455696195,
+ "name": "Super Mega Plan 2",
+ "price": "15.00",
+ "return_url": "http://yourapp.com",
+ "status": "rejected",
+ "test": null,
+ "trial_days": 0,
+ "trial_ends_on": null,
+ "updated_at": "2015-01-16T11:46:56-05:00",
+ "decorated_return_url": "http://yourapp.com?charge_id=455696195"
+ }
+ ]
+}
diff --git a/test/fixtures/refund.json b/test/fixtures/refund.json
new file mode 100644
index 00000000..b1345997
--- /dev/null
+++ b/test/fixtures/refund.json
@@ -0,0 +1,116 @@
+{
+ "refund": {
+ "id": 509562969,
+ "order_id": 450789469,
+ "created_at": "2016-06-20T13:35:06-04:00",
+ "note": "it broke during shipping",
+ "restock": true,
+ "user_id": 799407056,
+ "refund_line_items": [
+ {
+ "id": 104689539,
+ "quantity": 1,
+ "line_item_id": 703073504,
+ "line_item": {
+ "id": 703073504,
+ "variant_id": 457924702,
+ "title": "IPod Nano - 8gb",
+ "quantity": 1,
+ "price": "199.00",
+ "grams": 200,
+ "sku": "IPOD2008BLACK",
+ "variant_title": "black",
+ "vendor": null,
+ "fulfillment_service": "manual",
+ "product_id": 632910392,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "name": "IPod Nano - 8gb - black",
+ "variant_inventory_management": "shopify",
+ "properties": [
+ ],
+ "product_exists": true,
+ "fulfillable_quantity": 1,
+ "total_discount": "0.00",
+ "fulfillment_status": null,
+ "tax_lines": [
+ {
+ "title": "State Tax",
+ "price": "3.98",
+ "rate": 0.06
+ }
+ ]
+ }
+ },
+ {
+ "id": 709875399,
+ "quantity": 1,
+ "line_item_id": 466157049,
+ "line_item": {
+ "id": 466157049,
+ "variant_id": 39072856,
+ "title": "IPod Nano - 8gb",
+ "quantity": 1,
+ "price": "199.00",
+ "grams": 200,
+ "sku": "IPOD2008GREEN",
+ "variant_title": "green",
+ "vendor": null,
+ "fulfillment_service": "manual",
+ "product_id": 632910392,
+ "requires_shipping": true,
+ "taxable": true,
+ "gift_card": false,
+ "name": "IPod Nano - 8gb - green",
+ "variant_inventory_management": "shopify",
+ "properties": [
+ {
+ "name": "Custom Engraving Front",
+ "value": "Happy Birthday"
+ },
+ {
+ "name": "Custom Engraving Back",
+ "value": "Merry Christmas"
+ }
+ ],
+ "product_exists": true,
+ "fulfillable_quantity": 1,
+ "total_discount": "0.00",
+ "fulfillment_status": null,
+ "tax_lines": [
+ {
+ "title": "State Tax",
+ "price": "3.98",
+ "rate": 0.06
+ }
+ ]
+ }
+ }
+ ],
+ "transactions": [
+ {
+ "id": 179259969,
+ "order_id": 450789469,
+ "amount": "209.00",
+ "kind": "refund",
+ "gateway": "bogus",
+ "status": "success",
+ "message": null,
+ "created_at": "2005-08-05T12:59:12-04:00",
+ "test": false,
+ "authorization": "authorization-key",
+ "currency": "USD",
+ "location_id": null,
+ "user_id": null,
+ "parent_id": null,
+ "device_id": null,
+ "receipt": {},
+ "error_code": null,
+ "source_name": "web"
+ }
+ ],
+ "order_adjustments": [
+ ]
+ }
+}
diff --git a/test/fixtures/refund_calculate.json b/test/fixtures/refund_calculate.json
new file mode 100644
index 00000000..dbd06213
--- /dev/null
+++ b/test/fixtures/refund_calculate.json
@@ -0,0 +1,31 @@
+{
+ "refund": {
+ "shipping": {
+ "amount": "5.00",
+ "tax": "0.00",
+ "maximum_refundable": "5.00"
+ },
+ "refund_line_items": [
+ {
+ "quantity": 1,
+ "line_item_id": 518995019,
+ "price": "199.00",
+ "subtotal": "195.67",
+ "total_tax": "3.98",
+ "discounted_price": "199.00",
+ "discounted_total_price": "199.00",
+ "total_cart_discount_amount": "3.33"
+ }
+ ],
+ "transactions": [
+ {
+ "order_id": 450789469,
+ "amount": "41.94",
+ "kind": "suggested_refund",
+ "gateway": "bogus",
+ "parent_id": 801038806,
+ "maximum_refundable": "41.94"
+ }
+ ]
+ }
+}
diff --git a/test/fixtures/report.json b/test/fixtures/report.json
new file mode 100644
index 00000000..683b97e9
--- /dev/null
+++ b/test/fixtures/report.json
@@ -0,0 +1,9 @@
+{
+ "report": {
+ "id": 987,
+ "name": "Custom App Report",
+ "shopify_ql": "SHOW quantity_count, total_sales BY product_type, vendor, product_title FROM products SINCE -1m UNTIL -0m ORDER BY total_sales DESC",
+ "updated_at": "2017-04-12T14:00:54-04:00",
+ "category": "custom_app_reports"
+ }
+}
diff --git a/test/fixtures/reports.json b/test/fixtures/reports.json
new file mode 100644
index 00000000..bd829a6f
--- /dev/null
+++ b/test/fixtures/reports.json
@@ -0,0 +1,11 @@
+{
+ "reports": [
+ {
+ "id": 987,
+ "name": "Custom App Report",
+ "shopify_ql": "SHOW quantity_count, total_sales BY product_type, vendor, product_title FROM products SINCE -1m UNTIL -0m ORDER BY total_sales DESC",
+ "updated_at": "2017-04-12T14:00:54-04:00",
+ "category": "custom_app_reports"
+ }
+ ]
+}
diff --git a/test/fixtures/shipping_zones.json b/test/fixtures/shipping_zones.json
new file mode 100644
index 00000000..877534a7
--- /dev/null
+++ b/test/fixtures/shipping_zones.json
@@ -0,0 +1,114 @@
+{
+ "shipping_zones": [
+ {
+ "id": 1,
+ "name": "Some zone",
+ "countries": [
+ {
+ "id": 817138619,
+ "name": "United States",
+ "tax": 0.0,
+ "code": "US",
+ "tax_name": "Federal Tax",
+ "provinces": [
+ {
+ "id": 1013111685,
+ "country_id": 817138619,
+ "name": "New York",
+ "code": "NY",
+ "tax": 0.04,
+ "tax_name": "Tax",
+ "tax_type": null,
+ "shipping_zone_id": 1,
+ "tax_percentage": 4.0
+ },
+ {
+ "id": 1069646654,
+ "country_id": 817138619,
+ "name": "Ohio",
+ "code": "OH",
+ "tax": 0.0,
+ "tax_name": "State Tax",
+ "tax_type": null,
+ "shipping_zone_id": 1,
+ "tax_percentage": 0.0
+ }
+ ]
+ },
+ {
+ "id": 879921427,
+ "name": "Canada",
+ "tax": 0.05,
+ "code": "CA",
+ "tax_name": "GST",
+ "provinces": [
+ {
+ "id": 702530425,
+ "country_id": 879921427,
+ "name": "Ontario",
+ "code": "ON",
+ "tax": 0.08,
+ "tax_name": "Tax",
+ "tax_type": null,
+ "shipping_zone_id": 1,
+ "tax_percentage": 8.0
+ },
+ {
+ "id": 224293623,
+ "country_id": 879921427,
+ "name": "Quebec",
+ "code": "QC",
+ "tax": 0.09,
+ "tax_name": "HST",
+ "tax_type": "compounded",
+ "shipping_zone_id": 1,
+ "tax_percentage": 9.0
+ }
+ ]
+ },
+ {
+ "id": 988409122,
+ "name": "Yemen",
+ "tax": 0.0,
+ "code": "YE",
+ "tax_name": "GST",
+ "provinces": [
+ ]
+ }
+ ],
+ "weight_based_shipping_rates": [
+ {
+ "id": 760465697,
+ "weight_low": 1.2,
+ "weight_high": 10.0,
+ "name": "Austria Express Heavy Shipping",
+ "price": "40.00",
+ "shipping_zone_id": 1
+ }
+ ],
+ "price_based_shipping_rates": [
+ {
+ "id": 583276424,
+ "name": "Standard Shipping",
+ "min_order_subtotal": "0.00",
+ "price": "10.99",
+ "max_order_subtotal": "2000.00",
+ "shipping_zone_id": 1
+ }
+ ],
+ "carrier_shipping_rate_providers": [
+ {
+ "id": 972083812,
+ "country_id": null,
+ "carrier_service_id": 61629186,
+ "flat_modifier": "0.00",
+ "percent_modifier": 0,
+ "service_filter": {
+ "*": "+"
+ },
+ "shipping_zone_id": 1
+ }
+ ]
+ }
+ ]
+}
diff --git a/test/fixtures/shop.json b/test/fixtures/shop.json
new file mode 100644
index 00000000..ee925166
--- /dev/null
+++ b/test/fixtures/shop.json
@@ -0,0 +1,26 @@
+{
+ "shop": {
+ "name": "Apple Computers",
+ "city": "Cupertino",
+ "address1": "1 Infinite Loop",
+ "zip": "95014",
+ "created_at": "2007-12-31T19:00:00-05:00",
+ "shop_owner": "Steve Jobs",
+ "plan_name": "enterprise",
+ "public": false,
+ "country": "US",
+ "money_with_currency_format": "$ {{amount}} USD",
+ "money_format": "$ {{amount}}",
+ "domain": "shop.apple.com",
+ "taxes_included": null,
+ "id": 690933842,
+ "timezone": "(GMT-05:00) Eastern Time (US & Canada)",
+ "tax_shipping": null,
+ "phone": null,
+ "currency": "USD",
+ "myshopify_domain": "apple.myshopify.com",
+ "source": null,
+ "province": "CA",
+ "email": "steve@apple.com"
+ }
+}
diff --git a/test/fixtures/storefront_access_token.json b/test/fixtures/storefront_access_token.json
new file mode 100644
index 00000000..2b87ca0d
--- /dev/null
+++ b/test/fixtures/storefront_access_token.json
@@ -0,0 +1,9 @@
+{
+ "storefront_access_token": {
+ "id": 1,
+ "access_token": "477697f16c722efd66918cff7b3657a7",
+ "access_scope": "unauthenticated_read_product_listings",
+ "created_at": "2016-11-15T14:15:10-05:00",
+ "title": "Test"
+ }
+}
diff --git a/test/fixtures/storefront_access_tokens.json b/test/fixtures/storefront_access_tokens.json
new file mode 100644
index 00000000..cad2bc54
--- /dev/null
+++ b/test/fixtures/storefront_access_tokens.json
@@ -0,0 +1,18 @@
+{
+ "storefront_access_tokens": [
+ {
+ "id": 1,
+ "access_token": "477697f16c722efd66918cff7b3657a7",
+ "access_scope": "unauthenticated_read_product_listings",
+ "created_at": "2016-11-15T14:15:10-05:00",
+ "title": "Test 1"
+ },
+ {
+ "id": 2,
+ "access_token": "477697f16c722efd66918cff7b3657a7",
+ "access_scope": "unauthenticated_read_product_listings",
+ "created_at": "2016-11-15T14:15:10-05:00",
+ "title": "Test 2"
+ }
+ ]
+}
diff --git a/test/fixtures/tags.json b/test/fixtures/tags.json
new file mode 100644
index 00000000..876652ef
--- /dev/null
+++ b/test/fixtures/tags.json
@@ -0,0 +1 @@
+{"tags": ["consequuntur", "cupiditate", "repellendus"]}
diff --git a/test/fixtures/tender_transactions.json b/test/fixtures/tender_transactions.json
new file mode 100644
index 00000000..83e6d567
--- /dev/null
+++ b/test/fixtures/tender_transactions.json
@@ -0,0 +1,52 @@
+{
+ "tender_transactions": [
+ {
+ "id": 1,
+ "order_id": 450789469,
+ "amount": "138.46",
+ "currency": "CAD",
+ "user_id": null,
+ "test": true,
+ "processed_at": "2018-08-09T15:43:39-04:00",
+ "updated_at": "2018-08-09T15:43:41-04:00",
+ "remote_reference": "1118366",
+ "payment_method": "credit_card",
+ "payment_details": {
+ "credit_card_number": "•••• •••• •••• 1",
+ "credit_card_company": "Bogus"
+ }
+ },
+ {
+ "id": 2,
+ "order_id": 450789469,
+ "amount": "128.16",
+ "currency": "CAD",
+ "user_id": null,
+ "test": true,
+ "processed_at": "2018-08-11T15:43:39-04:00",
+ "updated_at": "2018-08-09T15:43:41-04:00",
+ "remote_reference": "1118367",
+ "payment_method": "credit_card",
+ "payment_details": {
+ "credit_card_number": "•••• •••• •••• 2",
+ "credit_card_company": "Bogus"
+ }
+ },
+ {
+ "id": 3,
+ "order_id": 450789469,
+ "amount": "28.16",
+ "currency": "CAD",
+ "user_id": null,
+ "test": true,
+ "processed_at": "2018-08-12T15:43:39-04:00",
+ "updated_at": "2018-08-09T15:43:41-04:00",
+ "remote_reference": "1118368",
+ "payment_method": "credit_card",
+ "payment_details": {
+ "credit_card_number": "•••• •••• •••• 3",
+ "credit_card_company": "Bogus"
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/transaction.json b/test/fixtures/transaction.json
new file mode 100644
index 00000000..61ae6f98
--- /dev/null
+++ b/test/fixtures/transaction.json
@@ -0,0 +1,29 @@
+{
+ "transaction": {
+ "amount": "409.94",
+ "authorization": "authorization-key",
+ "created_at": "2005-08-01T11:57:11-04:00",
+ "gateway": "bogus",
+ "id": 389404469,
+ "kind": "authorization",
+ "location_id": null,
+ "message": null,
+ "order_id": 450789469,
+ "parent_id": null,
+ "status": "success",
+ "test": false,
+ "user_id": null,
+ "device_id": null,
+ "receipt": {
+ "testcase": true,
+ "authorization": "123456"
+ },
+ "payment_details": {
+ "avs_result_code": null,
+ "credit_card_bin": null,
+ "cvv_result_code": null,
+ "credit_card_number": "XXXX-XXXX-XXXX-4242",
+ "credit_card_company": "Visa"
+ }
+ }
+}
diff --git a/test/fixtures/transactions.json b/test/fixtures/transactions.json
new file mode 100644
index 00000000..b712d649
--- /dev/null
+++ b/test/fixtures/transactions.json
@@ -0,0 +1,29 @@
+[
+ {
+ "amount": "409.94",
+ "authorization": "authorization-key",
+ "created_at": "2005-08-01T11:57:11-04:00",
+ "gateway": "bogus",
+ "id": 389404469,
+ "kind": "authorization",
+ "location_id": null,
+ "message": null,
+ "order_id": 450789469,
+ "parent_id": null,
+ "status": "success",
+ "test": false,
+ "user_id": null,
+ "device_id": null,
+ "receipt": {
+ "testcase": true,
+ "authorization": "123456"
+ },
+ "payment_details": {
+ "avs_result_code": null,
+ "credit_card_bin": null,
+ "cvv_result_code": null,
+ "credit_card_number": "XXXX-XXXX-XXXX-4242",
+ "credit_card_company": "Visa"
+ }
+ }
+]
diff --git a/test/fixtures/usage_charge.json b/test/fixtures/usage_charge.json
new file mode 100644
index 00000000..5ebfb3d5
--- /dev/null
+++ b/test/fixtures/usage_charge.json
@@ -0,0 +1,11 @@
+{
+ "usage_charge": {
+ "id": 359376002,
+ "description": "1000 emails",
+ "price": "9.00",
+ "recurring_application_charge_id": 654381177,
+ "billing_on": "2015-07-08",
+ "balance_remaining": "91.00",
+ "balance_used": "9.00"
+ }
+}
diff --git a/test/fixtures/usage_charges.json b/test/fixtures/usage_charges.json
new file mode 100644
index 00000000..f29c6e7f
--- /dev/null
+++ b/test/fixtures/usage_charges.json
@@ -0,0 +1,23 @@
+{
+ "usage_charges": [
+ {
+ "id": 359376005,
+ "description": "1000 emails",
+ "price": "9.00",
+ "recurring_application_charge_id": 654381177,
+ "billing_on": "2015-07-08",
+ "balance_remaining": "82.00",
+ "balance_used": "18.00"
+
+ },
+ {
+ "id": 359376006,
+ "description": "1000 emails",
+ "price": "9.00",
+ "recurring_application_charge_id": 654381177,
+ "billing_on": "2015-07-08",
+ "balance_remaining": "82.00",
+ "balance_used": "18.00"
+ }
+ ]
+}
diff --git a/test/fixtures/user.json b/test/fixtures/user.json
new file mode 100644
index 00000000..d0c5890a
--- /dev/null
+++ b/test/fixtures/user.json
@@ -0,0 +1,23 @@
+{
+ "user": {
+ "id": 799407056,
+ "first_name": "Steve",
+ "email": "steve@apple.com",
+ "url": "www.apple.com",
+ "im": null,
+ "screen_name": null,
+ "phone": null,
+ "last_name": "Jobs",
+ "account_owner": true,
+ "receive_announcements": 1,
+ "bio": null,
+ "permissions": [
+ "full"
+ ],
+ "locale": "en",
+ "user_type": "regular",
+ "phone_validated?": false,
+ "tfa_enabled?": false,
+ "admin_graphql_api_id": "gid://shopify/StaffMember/799407056"
+ }
+}
diff --git a/test/fixtures/users.json b/test/fixtures/users.json
new file mode 100644
index 00000000..83cb01eb
--- /dev/null
+++ b/test/fixtures/users.json
@@ -0,0 +1,44 @@
+{
+ "users": [
+ {
+ "id": 799407056,
+ "first_name": "Steve",
+ "email": "steve@apple.com",
+ "url": "www.apple.com",
+ "im": null,
+ "screen_name": null,
+ "phone": null,
+ "last_name": "Jobs",
+ "account_owner": true,
+ "receive_announcements": 1,
+ "bio": null,
+ "permissions": [
+ "full"
+ ],
+ "locale": "en",
+ "user_type": "regular",
+ "phone_validated?": false,
+ "tfa_enabled?": false,
+ "admin_graphql_api_id": "gid://shopify/StaffMember/799407056"
+ },
+ {
+ "id": 930143300,
+ "first_name": "noaccesssteve",
+ "email": "noaccesssteve@jobs.com",
+ "url": "www.apple.com",
+ "im": null,
+ "screen_name": null,
+ "phone": null,
+ "last_name": "Jobs",
+ "account_owner": false,
+ "receive_announcements": 1,
+ "bio": null,
+ "permissions": [],
+ "locale": "en",
+ "user_type": "regular",
+ "phone_validated?": false,
+ "tfa_enabled?": false,
+ "admin_graphql_api_id": "gid://shopify/StaffMember/930143300"
+ }
+ ]
+}
diff --git a/test/fixtures/variant.json b/test/fixtures/variant.json
new file mode 100644
index 00000000..173d7c33
--- /dev/null
+++ b/test/fixtures/variant.json
@@ -0,0 +1,22 @@
+{
+ "variant": {
+ "price": "199.00",
+ "position": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "title": "Pink",
+ "requires_shipping": true,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "inventory_quantity": 10,
+ "inventory_management": "shopify",
+ "taxable": true,
+ "id": 808950810,
+ "grams": 200,
+ "sku": "IPOD2008PINK",
+ "option1": "Pink",
+ "option2": null,
+ "fulfillment_service": "manual",
+ "option3": null
+ }
+}
diff --git a/test/fixtures/variants.json b/test/fixtures/variants.json
new file mode 100644
index 00000000..1128180a
--- /dev/null
+++ b/test/fixtures/variants.json
@@ -0,0 +1,84 @@
+{
+ "variants": [
+ {
+ "price": "199.00",
+ "position": 1,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "title": "Pink",
+ "requires_shipping": true,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "inventory_quantity": 10,
+ "inventory_management": "shopify",
+ "taxable": true,
+ "id": 808950810,
+ "grams": 200,
+ "sku": "IPOD2008PINK",
+ "option1": "Pink",
+ "option2": null,
+ "fulfillment_service": "manual",
+ "option3": null
+ },
+ {
+ "price": "199.00",
+ "position": 2,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "title": "Red",
+ "requires_shipping": true,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "inventory_quantity": 20,
+ "inventory_management": "shopify",
+ "taxable": true,
+ "id": 49148385,
+ "grams": 200,
+ "sku": "IPOD2008RED",
+ "option1": "Red",
+ "option2": null,
+ "fulfillment_service": "manual",
+ "option3": null
+ },
+ {
+ "price": "199.00",
+ "position": 3,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "title": "Green",
+ "requires_shipping": true,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "inventory_quantity": 30,
+ "inventory_management": "shopify",
+ "taxable": true,
+ "id": 39072856,
+ "grams": 200,
+ "sku": "IPOD2008GREEN",
+ "option1": "Green",
+ "option2": null,
+ "fulfillment_service": "manual",
+ "option3": null
+ },
+ {
+ "price": "199.00",
+ "position": 4,
+ "created_at": "2011-10-20T14:05:13-04:00",
+ "title": "Black",
+ "requires_shipping": true,
+ "updated_at": "2011-10-20T14:05:13-04:00",
+ "inventory_policy": "continue",
+ "compare_at_price": null,
+ "inventory_quantity": 40,
+ "inventory_management": "shopify",
+ "taxable": true,
+ "id": 457924702,
+ "grams": 200,
+ "sku": "IPOD2008BLACK",
+ "option1": "Black",
+ "option2": null,
+ "fulfillment_service": "manual",
+ "option3": null
+ }
+ ]
+}
diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py
new file mode 100644
index 00000000..df92c3b6
--- /dev/null
+++ b/test/fulfillment_event_test.py
@@ -0,0 +1,40 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class FulFillmentEventTest(TestCase):
+ def test_get_fulfillment_event(self):
+ self.fake(
+ "orders/2776493818019/fulfillments/2608403447971/events",
+ method="GET",
+ body=self.load_fixture("fulfillment_event"),
+ )
+ fulfillment_event = shopify.FulfillmentEvent.find(order_id=2776493818019, fulfillment_id=2608403447971)
+ self.assertEqual(1, len(fulfillment_event))
+
+ def test_create_fulfillment_event(self):
+ self.fake(
+ "orders/2776493818019/fulfillments/2608403447971/events",
+ method="POST",
+ body=self.load_fixture("fulfillment_event"),
+ headers={"Content-type": "application/json"},
+ )
+ new_fulfillment_event = shopify.FulfillmentEvent(
+ {"order_id": "2776493818019", "fulfillment_id": "2608403447971"}
+ )
+ new_fulfillment_event.status = "ready_for_pickup"
+ new_fulfillment_event.save()
+
+ def test_error_on_incorrect_status(self):
+ with self.assertRaises(AttributeError):
+ self.fake(
+ "orders/2776493818019/fulfillments/2608403447971/events/12584341209251",
+ method="GET",
+ body=self.load_fixture("fulfillment_event"),
+ )
+ incorrect_status = "asdf"
+ fulfillment_event = shopify.FulfillmentEvent.find(
+ 12584341209251, order_id="2776493818019", fulfillment_id="2608403447971"
+ )
+ fulfillment_event.status = incorrect_status
+ fulfillment_event.save()
diff --git a/test/fulfillment_service_test.py b/test/fulfillment_service_test.py
new file mode 100644
index 00000000..7519b3b9
--- /dev/null
+++ b/test/fulfillment_service_test.py
@@ -0,0 +1,26 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class FulfillmentServiceTest(TestCase):
+ def test_create_new_fulfillment_service(self):
+ self.fake(
+ "fulfillment_services",
+ method="POST",
+ body=self.load_fixture("fulfillment_service"),
+ headers={"Content-type": "application/json"},
+ )
+
+ fulfillment_service = shopify.FulfillmentService.create({"name": "SomeService"})
+ self.assertEqual("SomeService", fulfillment_service.name)
+
+ def test_get_fulfillment_service(self):
+ self.fake("fulfillment_services/123456", method="GET", body=self.load_fixture("fulfillment_service"))
+
+ fulfillment_service = shopify.FulfillmentService.find(123456)
+ self.assertEqual("SomeService", fulfillment_service.name)
+
+ def test_set_format_attribute(self):
+ fulfillment_service = shopify.FulfillmentService()
+ fulfillment_service.format = "json"
+ self.assertEqual("json", fulfillment_service.attributes["format"])
diff --git a/test/fulfillment_test.py b/test/fulfillment_test.py
new file mode 100644
index 00000000..8bb84efb
--- /dev/null
+++ b/test/fulfillment_test.py
@@ -0,0 +1,83 @@
+import shopify
+from test.test_helper import TestCase
+from pyactiveresource.activeresource import ActiveResource
+
+
+class FulFillmentTest(TestCase):
+ def setUp(self):
+ super(FulFillmentTest, self).setUp()
+ self.fake("orders/450789469/fulfillments/255858046", method="GET", body=self.load_fixture("fulfillment"))
+
+ def test_able_to_open_fulfillment(self):
+ fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
+
+ success = self.load_fixture("fulfillment")
+ success = success.replace(b"pending", b"open")
+ self.fake(
+ "orders/450789469/fulfillments/255858046/open",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=success,
+ )
+
+ self.assertEqual("pending", fulfillment.status)
+ fulfillment.open()
+ self.assertEqual("open", fulfillment.status)
+
+ def test_able_to_complete_fulfillment(self):
+ fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
+
+ success = self.load_fixture("fulfillment")
+ success = success.replace(b"pending", b"success")
+ self.fake(
+ "orders/450789469/fulfillments/255858046/complete",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=success,
+ )
+
+ self.assertEqual("pending", fulfillment.status)
+ fulfillment.complete()
+ self.assertEqual("success", fulfillment.status)
+
+ def test_able_to_cancel_fulfillment(self):
+ fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
+
+ cancelled = self.load_fixture("fulfillment")
+ cancelled = cancelled.replace(b"pending", b"cancelled")
+ self.fake(
+ "orders/450789469/fulfillments/255858046/cancel",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=cancelled,
+ )
+
+ self.assertEqual("pending", fulfillment.status)
+ fulfillment.cancel()
+ self.assertEqual("cancelled", fulfillment.status)
+
+ def test_update_tracking(self):
+ fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
+
+ tracking_info = {"number": 1111, "url": "http://www.my-url.com", "company": "my-company"}
+ notify_customer = False
+
+ update_tracking = self.load_fixture("fulfillment")
+ update_tracking = update_tracking.replace(b"null-company", b"my-company")
+ update_tracking = update_tracking.replace(b"http://www.google.com/search?q=1Z2345", b"http://www.my-url.com")
+ update_tracking = update_tracking.replace(b"1Z2345", b"1111")
+
+ self.fake(
+ "fulfillments/255858046/update_tracking",
+ method="POST",
+ headers={"Content-type": "application/json"},
+ body=update_tracking,
+ )
+
+ self.assertEqual("null-company", fulfillment.tracking_company)
+ self.assertEqual("1Z2345", fulfillment.tracking_number)
+ self.assertEqual("http://www.google.com/search?q=1Z2345", fulfillment.tracking_url)
+ fulfillment.update_tracking(tracking_info, notify_customer)
+ self.assertEqual("my-company", fulfillment.tracking_company)
+ self.assertEqual("1111", fulfillment.tracking_number)
+ self.assertEqual("http://www.my-url.com", fulfillment.tracking_url)
diff --git a/test/gift_card_test.py b/test/gift_card_test.py
new file mode 100644
index 00000000..2b5e6055
--- /dev/null
+++ b/test/gift_card_test.py
@@ -0,0 +1,65 @@
+from decimal import Decimal
+import shopify
+from test.test_helper import TestCase
+
+
+class GiftCardTest(TestCase):
+ def test_gift_card_creation(self):
+ self.fake(
+ "gift_cards",
+ method="POST",
+ code=202,
+ body=self.load_fixture("gift_card"),
+ headers={"Content-type": "application/json"},
+ )
+ gift_card = shopify.GiftCard.create({"code": "d7a2bcggda89c293", "note": "Gift card note."})
+ self.assertEqual("Gift card note.", gift_card.note)
+ self.assertEqual("c293", gift_card.last_characters)
+
+ def test_fetch_gift_cards(self):
+ self.fake("gift_cards", method="GET", code=200, body=self.load_fixture("gift_cards"))
+ gift_cards = shopify.GiftCard.find()
+ self.assertEqual(1, len(gift_cards))
+
+ def test_disable_gift_card(self):
+ self.fake("gift_cards/4208208", method="GET", code=200, body=self.load_fixture("gift_card"))
+ self.fake(
+ "gift_cards/4208208/disable",
+ method="POST",
+ code=200,
+ body=self.load_fixture("gift_card_disabled"),
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ )
+ gift_card = shopify.GiftCard.find(4208208)
+ self.assertFalse(gift_card.disabled_at)
+ gift_card.disable()
+ self.assertTrue(gift_card.disabled_at)
+
+ def test_adjust_gift_card(self):
+ self.fake("gift_cards/4208208", method="GET", code=200, body=self.load_fixture("gift_card"))
+ self.fake(
+ "gift_cards/4208208/adjustments",
+ method="POST",
+ code=201,
+ body=self.load_fixture("gift_card_adjustment"),
+ headers={"Content-type": "application/json"},
+ )
+ gift_card = shopify.GiftCard.find(4208208)
+ self.assertEqual(gift_card.balance, "25.00")
+ adjustment = gift_card.add_adjustment(
+ shopify.GiftCardAdjustment(
+ {
+ "amount": 100,
+ }
+ )
+ )
+ self.assertIsInstance(adjustment, shopify.GiftCardAdjustment)
+ self.assertEqual(Decimal(adjustment.amount), Decimal("100"))
+
+ def test_search(self):
+ self.fake(
+ "gift_cards/search.json?query=balance%3A10", extension=False, body=self.load_fixture("gift_cards_search")
+ )
+
+ results = shopify.GiftCard.search(query="balance:10")
+ self.assertEqual(results[0].balance, "10.00")
diff --git a/test/graphql_test.py b/test/graphql_test.py
new file mode 100644
index 00000000..dc32b935
--- /dev/null
+++ b/test/graphql_test.py
@@ -0,0 +1,46 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class GraphQLTest(TestCase):
+ def setUp(self):
+ super(GraphQLTest, self).setUp()
+ shopify.ApiVersion.define_known_versions()
+ shopify_session = shopify.Session("this-is-my-test-show.myshopify.com", "unstable", "token")
+ shopify.ShopifyResource.activate_session(shopify_session)
+ self.client = shopify.GraphQL()
+ self.fake(
+ "graphql",
+ method="POST",
+ code=201,
+ headers={
+ "X-Shopify-Access-Token": "token",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ },
+ )
+
+ def test_fetch_shop_with_graphql(self):
+ query = """
+ {
+ shop {
+ name
+ id
+ }
+ }
+ """
+ result = self.client.execute(query)
+ self.assertTrue(json.loads(result)["shop"]["name"] == "Apple Computers")
+
+ def test_specify_operation_name(self):
+ query = """
+ query GetShop{
+ shop {
+ name
+ id
+ }
+ }
+ """
+ result = self.client.execute(query, operation_name="GetShop")
+ self.assertTrue(json.loads(result)["shop"]["name"] == "Apple Computers")
diff --git a/test/image_test.py b/test/image_test.py
new file mode 100644
index 00000000..3dad3817
--- /dev/null
+++ b/test/image_test.py
@@ -0,0 +1,84 @@
+import shopify
+from test.test_helper import TestCase
+import base64
+
+
+class ImageTest(TestCase):
+ def test_create_image(self):
+ self.fake(
+ "products/632910392/images",
+ method="POST",
+ body=self.load_fixture("image"),
+ headers={"Content-type": "application/json"},
+ )
+ image = shopify.Image({"product_id": 632910392})
+ image.position = 1
+ image.attachment = "R0lGODlhbgCMAPf/APbr48VySrxTO7IgKt2qmKQdJeK8lsFjROG5p/nz7Zg3MNmnd7Q1MLNVS9GId71hSJMZIuzTu4UtKbeEeakhKMl8U8WYjfr18YQaIbAf=="
+ image.save()
+
+ self.assertEqual(
+ "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540", image.src
+ )
+ self.assertEqual(850703190, image.id)
+
+ def test_attach_image(self):
+ self.fake(
+ "products/632910392/images",
+ method="POST",
+ body=self.load_fixture("image"),
+ headers={"Content-type": "application/json"},
+ )
+ image = shopify.Image({"product_id": 632910392})
+ image.position = 1
+ binary_in = base64.b64decode(
+ "R0lGODlhbgCMAPf/APbr48VySrxTO7IgKt2qmKQdJeK8lsFjROG5p/nz7Zg3MNmnd7Q1MLNVS9GId71hSJMZIuzTu4UtKbeEeakhKMl8U8WYjfr18YQaIbAf=="
+ )
+ image.attach_image(data=binary_in, filename="ipod-nano.png")
+ image.save()
+ binary_out = base64.b64decode(image.attachment)
+
+ self.assertEqual(
+ "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540", image.src
+ )
+ self.assertEqual(850703190, image.id)
+ self.assertEqual(binary_in, binary_out)
+
+ def test_create_image_then_add_parent_id(self):
+ self.fake(
+ "products/632910392/images",
+ method="POST",
+ body=self.load_fixture("image"),
+ headers={"Content-type": "application/json"},
+ )
+ image = shopify.Image()
+ image.position = 1
+ image.product_id = 632910392
+ image.attachment = "R0lGODlhbgCMAPf/APbr48VySrxTO7IgKt2qmKQdJeK8lsFjROG5p/nz7Zg3MNmnd7Q1MLNVS9GId71hSJMZIuzTu4UtKbeEeakhKMl8U8WYjfr18YQaIbAf=="
+ image.save()
+
+ self.assertEqual(
+ "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540", image.src
+ )
+ self.assertEqual(850703190, image.id)
+
+ def test_get_images(self):
+ self.fake("products/632910392/images", method="GET", body=self.load_fixture("images"))
+ image = shopify.Image.find(product_id=632910392)
+ self.assertEqual(2, len(image))
+
+ def test_get_image(self):
+ self.fake("products/632910392/images/850703190", method="GET", body=self.load_fixture("image"))
+ image = shopify.Image.find(850703190, product_id=632910392)
+ self.assertEqual(850703190, image.id)
+
+ def test_get_metafields_for_image(self):
+ fake_extension = "json?metafield[owner_id]=850703190&metafield[owner_resource]=product_image"
+ self.fake("metafields", method="GET", extension=fake_extension, body=self.load_fixture("image_metafields"))
+
+ image = shopify.Image(attributes={"id": 850703190, "product_id": 632910392})
+ metafields = image.metafields()
+
+ self.assertEqual(1, len(metafields))
+ for field in metafields:
+ self.assertTrue(isinstance(field, shopify.Metafield))
+ self.assertEqual(metafields[0].value, "Image Alt Tag")
diff --git a/test/inventory_item_test.py b/test/inventory_item_test.py
new file mode 100644
index 00000000..61c66e60
--- /dev/null
+++ b/test/inventory_item_test.py
@@ -0,0 +1,19 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class InventoryItemTest(TestCase):
+ def test_fetch_inventory_item(self):
+ self.fake("inventory_items/123456789", method="GET", body=self.load_fixture("inventory_item"))
+ inventory_item = shopify.InventoryItem.find(123456789)
+ self.assertEqual(inventory_item.sku, "IPOD2008PINK")
+
+ def test_fetch_inventory_item_ids(self):
+ self.fake(
+ "inventory_items.json?ids=123456789%2C234567891",
+ extension="",
+ method="GET",
+ body=self.load_fixture("inventory_items"),
+ )
+ inventory_items = shopify.InventoryItem.find(ids="123456789,234567891")
+ self.assertEqual(3, len(inventory_items))
diff --git a/test/inventory_level_test.py b/test/inventory_level_test.py
new file mode 100644
index 00000000..cf621b7c
--- /dev/null
+++ b/test/inventory_level_test.py
@@ -0,0 +1,68 @@
+import shopify
+import json
+from six.moves.urllib.parse import urlencode
+from test.test_helper import TestCase
+
+
+class InventoryLevelTest(TestCase):
+ def test_fetch_inventory_level(self):
+ params = {"inventory_item_ids": [808950810, 39072856], "location_ids": [905684977, 487838322]}
+
+ self.fake(
+ "inventory_levels.json?location_ids=905684977%2C487838322&inventory_item_ids=808950810%2C39072856",
+ method="GET",
+ extension="",
+ body=self.load_fixture("inventory_levels"),
+ )
+ inventory_levels = shopify.InventoryLevel.find(
+ inventory_item_ids="808950810,39072856", location_ids="905684977,487838322"
+ )
+ self.assertTrue(
+ all(
+ item.location_id in params["location_ids"] and item.inventory_item_id in params["inventory_item_ids"]
+ for item in inventory_levels
+ )
+ )
+
+ def test_inventory_level_adjust(self):
+ self.fake(
+ "inventory_levels/adjust",
+ method="POST",
+ body=self.load_fixture("inventory_level"),
+ headers={"Content-type": "application/json"},
+ )
+ inventory_level = shopify.InventoryLevel.adjust(905684977, 808950810, 5)
+ self.assertEqual(inventory_level.available, 6)
+
+ def test_inventory_level_connect(self):
+ self.fake(
+ "inventory_levels/connect",
+ method="POST",
+ body=self.load_fixture("inventory_level"),
+ headers={"Content-type": "application/json"},
+ code=201,
+ )
+ inventory_level = shopify.InventoryLevel.connect(905684977, 808950810)
+ self.assertEqual(inventory_level.available, 6)
+
+ def test_inventory_level_set(self):
+ self.fake(
+ "inventory_levels/set",
+ method="POST",
+ body=self.load_fixture("inventory_level"),
+ headers={"Content-type": "application/json"},
+ )
+ inventory_level = shopify.InventoryLevel.set(905684977, 808950810, 6)
+ self.assertEqual(inventory_level.available, 6)
+
+ def test_destroy_inventory_level(self):
+ inventory_level_response = json.loads(self.load_fixture("inventory_level").decode())
+ inventory_level = shopify.InventoryLevel(inventory_level_response["inventory_level"])
+
+ query_params = urlencode(
+ {"inventory_item_id": inventory_level.inventory_item_id, "location_id": inventory_level.location_id}
+ )
+ path = "inventory_levels.json?" + query_params
+
+ self.fake(path, extension=False, method="DELETE", code=204, body="{}")
+ inventory_level.destroy()
diff --git a/test/limits_test.py b/test/limits_test.py
new file mode 100644
index 00000000..0d705abb
--- /dev/null
+++ b/test/limits_test.py
@@ -0,0 +1,65 @@
+import shopify
+from mock import patch
+from test.test_helper import TestCase
+
+
+class LimitsTest(TestCase):
+ """
+ API Calls Limit Tests
+
+ Conversion of test/limits_test.rb
+ """
+
+ @classmethod
+ def setUpClass(self):
+ self.original_headers = None
+
+ def setUp(self):
+ super(LimitsTest, self).setUp()
+ self.fake("shop")
+ shopify.Shop.current()
+ # TODO: Fake not support Headers
+ self.original_headers = shopify.Shop.connection.response.headers
+
+ def tearDown(self):
+ super(LimitsTest, self).tearDown()
+ shopify.Shop.connection.response.headers = self.original_headers
+
+ def test_raise_error_no_header(self):
+ with self.assertRaises(Exception):
+ shopify.Limits.credit_left()
+
+ def test_raise_error_invalid_header(self):
+ with patch.dict(shopify.Shop.connection.response.headers, {"bad": "value"}, clear=True):
+ with self.assertRaises(Exception):
+ shopify.Limits.credit_left()
+
+ def test_fetch_limits_total(self):
+ with patch.dict(
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "40/40"}, clear=True
+ ):
+ self.assertEqual(40, shopify.Limits.credit_limit())
+
+ def test_fetch_used_calls(self):
+ with patch.dict(
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "1/40"}, clear=True
+ ):
+ self.assertEqual(1, shopify.Limits.credit_used())
+
+ def test_calculate_remaining_calls(self):
+ with patch.dict(
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "292/300"}, clear=True
+ ):
+ self.assertEqual(8, shopify.Limits.credit_left())
+
+ def test_maxed_credits_false(self):
+ with patch.dict(
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "125/300"}, clear=True
+ ):
+ self.assertFalse(shopify.Limits.credit_maxed())
+
+ def test_maxed_credits_true(self):
+ with patch.dict(
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "40/40"}, clear=True
+ ):
+ self.assertTrue(shopify.Limits.credit_maxed())
diff --git a/test/location_test.py b/test/location_test.py
new file mode 100644
index 00000000..8550b1af
--- /dev/null
+++ b/test/location_test.py
@@ -0,0 +1,31 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class LocationTest(TestCase):
+ def test_fetch_locations(self):
+ self.fake("locations", method="GET", body=self.load_fixture("locations"))
+ locations = shopify.Location.find()
+ self.assertEqual(2, len(locations))
+
+ def test_fetch_location(self):
+ self.fake("locations/487838322", method="GET", body=self.load_fixture("location"))
+ location = shopify.Location.find(487838322)
+ self.assertEqual(location.id, 487838322)
+ self.assertEqual(location.name, "Fifth Avenue AppleStore")
+
+ def test_inventory_levels_returns_all_inventory_levels(self):
+ location = shopify.Location({"id": 487838322})
+
+ self.fake(
+ "locations/%s/inventory_levels" % location.id,
+ method="GET",
+ code=200,
+ body=self.load_fixture("location_inventory_levels"),
+ )
+ inventory_levels = location.inventory_levels()
+
+ self.assertEqual(location.id, inventory_levels[0].location_id)
+ self.assertEqual(27, inventory_levels[0].available)
+ self.assertEqual(9, inventory_levels[1].available)
diff --git a/test/marketing_event_test.py b/test/marketing_event_test.py
new file mode 100644
index 00000000..2f73029d
--- /dev/null
+++ b/test/marketing_event_test.py
@@ -0,0 +1,97 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class MarketingEventTest(TestCase):
+ def test_get_marketing_event(self):
+ self.fake("marketing_events/1", method="GET", body=self.load_fixture("marketing_event"))
+ marketing_event = shopify.MarketingEvent.find(1)
+ self.assertEqual(marketing_event.id, 1)
+
+ def test_get_marketing_events(self):
+ self.fake("marketing_events", method="GET", body=self.load_fixture("marketing_events"))
+ marketing_events = shopify.MarketingEvent.find()
+ self.assertEqual(len(marketing_events), 2)
+
+ def test_create_marketing_event(self):
+ self.fake(
+ "marketing_events",
+ method="POST",
+ body=self.load_fixture("marketing_event"),
+ headers={"Content-type": "application/json"},
+ )
+
+ marketing_event = shopify.MarketingEvent()
+ marketing_event.currency_code = "GBP"
+ marketing_event.event_target = "facebook"
+ marketing_event.event_type = "post"
+ marketing_event.save()
+
+ self.assertEqual(marketing_event.event_target, "facebook")
+ self.assertEqual(marketing_event.currency_code, "GBP")
+ self.assertEqual(marketing_event.event_type, "post")
+
+ def test_delete_marketing_event(self):
+ self.fake("marketing_events/1", method="GET", body=self.load_fixture("marketing_event"))
+ self.fake("marketing_events/1", method="DELETE", body="destroyed")
+
+ marketing_event = shopify.MarketingEvent.find(1)
+ marketing_event.destroy()
+
+ self.assertEqual("DELETE", self.http.request.get_method())
+
+ def test_update_marketing_event(self):
+ self.fake("marketing_events/1", method="GET", code=200, body=self.load_fixture("marketing_event"))
+ self.fake(
+ "marketing_events/1",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("marketing_event"),
+ headers={"Content-type": "application/json"},
+ )
+
+ marketing_event = shopify.MarketingEvent.find(1)
+ marketing_event.currency = "USD"
+
+ self.assertTrue(marketing_event.save())
+
+ def test_count_marketing_events(self):
+ self.fake("marketing_events/count", method="GET", body='{"count": 2}')
+ marketing_events_count = shopify.MarketingEvent.count()
+ self.assertEqual(marketing_events_count, 2)
+
+ def test_add_engagements(self):
+ self.fake("marketing_events/1", method="GET", body=self.load_fixture("marketing_event"))
+ self.fake(
+ "marketing_events/1/engagements",
+ method="POST",
+ code=201,
+ body=self.load_fixture("engagement"),
+ headers={"Content-type": "application/json"},
+ )
+
+ marketing_event = shopify.MarketingEvent.find(1)
+ response = marketing_event.add_engagements(
+ [
+ {
+ "occurred_on": "2017-04-20",
+ "impressions_count": None,
+ "views_count": None,
+ "clicks_count": 10,
+ "shares_count": None,
+ "favorites_count": None,
+ "comments_count": None,
+ "ad_spend": None,
+ "is_cumulative": True,
+ }
+ ]
+ )
+
+ request_data = json.loads(self.http.request.data.decode("utf-8"))["engagements"]
+ self.assertEqual(len(request_data), 1)
+ self.assertEqual(request_data[0]["occurred_on"], "2017-04-20")
+
+ response_data = json.loads(response.body.decode("utf-8"))["engagements"]
+ self.assertEqual(len(response_data), 1)
+ self.assertEqual(response_data[0]["occurred_on"], "2017-04-20")
diff --git a/test/order_risk_test.py b/test/order_risk_test.py
new file mode 100644
index 00000000..4ca6dbff
--- /dev/null
+++ b/test/order_risk_test.py
@@ -0,0 +1,52 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class OrderRiskTest(TestCase):
+ def test_create_order_risk(self):
+ self.fake(
+ "orders/450789469/risks",
+ method="POST",
+ body=self.load_fixture("order_risk"),
+ headers={"Content-type": "application/json"},
+ )
+ v = shopify.OrderRisk({"order_id": 450789469})
+ v.message = "This order was placed from a proxy IP"
+ v.recommendation = "cancel"
+ v.score = "1.0"
+ v.source = "External"
+ v.merchant_message = "This order was placed from a proxy IP"
+ v.display = True
+ v.cause_cancel = True
+ v.save()
+
+ self.assertEqual(284138680, v.id)
+
+ def test_get_order_risks(self):
+ self.fake("orders/450789469/risks", method="GET", body=self.load_fixture("order_risks"))
+ v = shopify.OrderRisk.find(order_id=450789469)
+ self.assertEqual(2, len(v))
+
+ def test_get_order_risk(self):
+ self.fake("orders/450789469/risks/284138680", method="GET", body=self.load_fixture("order_risk"))
+ v = shopify.OrderRisk.find(284138680, order_id=450789469)
+ self.assertEqual(284138680, v.id)
+
+ def test_delete_order_risk(self):
+ self.fake("orders/450789469/risks/284138680", method="GET", body=self.load_fixture("order_risk"))
+ self.fake("orders/450789469/risks/284138680", method="DELETE", body="destroyed")
+ v = shopify.OrderRisk.find(284138680, order_id=450789469)
+ v.destroy()
+
+ def test_delete_order_risk(self):
+ self.fake("orders/450789469/risks/284138680", method="GET", body=self.load_fixture("order_risk"))
+ self.fake(
+ "orders/450789469/risks/284138680",
+ method="PUT",
+ body=self.load_fixture("order_risk"),
+ headers={"Content-type": "application/json"},
+ )
+
+ v = shopify.OrderRisk.find(284138680, order_id=450789469)
+ v.position = 3
+ v.save()
diff --git a/test/order_test.py b/test/order_test.py
new file mode 100644
index 00000000..257ab08a
--- /dev/null
+++ b/test/order_test.py
@@ -0,0 +1,56 @@
+import shopify
+from test.test_helper import TestCase
+from pyactiveresource.activeresource import ActiveResource
+from pyactiveresource.util import xml_to_dict
+
+
+class OrderTest(TestCase):
+ def test_should_be_loaded_correctly_from_order_xml(self):
+ order_xml = """
+
+
+
+ size
+ large
+
+
+ """
+ order = shopify.Order(xml_to_dict(order_xml)["order"])
+
+ self.assertEqual(1, len(order.note_attributes))
+
+ note_attribute = order.note_attributes[0]
+ self.assertEqual("size", note_attribute.name)
+ self.assertEqual("large", note_attribute.value)
+
+ def test_should_be_able_to_add_note_attributes_to_an_order(self):
+ order = shopify.Order()
+ order.note_attributes = []
+ order.note_attributes.append(shopify.NoteAttribute({"name": "color", "value": "blue"}))
+
+ order_xml = xml_to_dict(order.to_xml())
+ note_attributes = order_xml["order"]["note_attributes"]
+ self.assertTrue(isinstance(note_attributes, list))
+
+ attribute = note_attributes[0]
+ self.assertEqual("color", attribute["name"])
+ self.assertEqual("blue", attribute["value"])
+
+ def test_get_order(self):
+ self.fake("orders/450789469", method="GET", body=self.load_fixture("order"))
+ order = shopify.Order.find(450789469)
+ self.assertEqual("bob.norman@hostmail.com", order.email)
+
+ def test_get_order_transaction(self):
+ self.fake("orders/450789469", method="GET", body=self.load_fixture("order"))
+ order = shopify.Order.find(450789469)
+ self.fake("orders/450789469/transactions", method="GET", body=self.load_fixture("transactions"))
+ transactions = order.transactions()
+ self.assertEqual("409.94", transactions[0].amount)
+
+ def test_get_customer_orders(self):
+ self.fake("customers/207119551/orders", method="GET", body=self.load_fixture("orders"), code=200)
+ orders = shopify.Order.find(customer_id=207119551)
+ self.assertIsInstance(orders[0], shopify.Order)
+ self.assertEqual(450789469, orders[0].id)
+ self.assertEqual(207119551, orders[0].customer.id)
diff --git a/test/pagination_test.py b/test/pagination_test.py
new file mode 100644
index 00000000..72fc78e9
--- /dev/null
+++ b/test/pagination_test.py
@@ -0,0 +1,124 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class PaginationTest(TestCase):
+ def setUp(self):
+ super(PaginationTest, self).setUp()
+ prefix = self.http.site + "/admin/api/unstable"
+ fixture = json.loads(self.load_fixture("products").decode())
+
+ self.next_page_url = prefix + "/products.json?limit=2&page_info=FOOBAR"
+ self.prev_page_url = prefix + "/products.json?limit=2&page_info=BAZQUUX"
+
+ next_headers = {"Link": "<" + self.next_page_url + '>; rel="next"'}
+ prev_headers = {"Link": "<" + self.prev_page_url + '>; rel="previous"'}
+
+ self.fake(
+ "products",
+ url=prefix + "/products.json?limit=2",
+ body=json.dumps({"products": fixture[:2]}),
+ response_headers=next_headers,
+ )
+ self.fake(
+ "products",
+ url=prefix + "/products.json?limit=2&page_info=FOOBAR",
+ body=json.dumps({"products": fixture[2:4]}),
+ response_headers=prev_headers,
+ )
+ self.fake(
+ "products",
+ url=prefix + "/products.json?limit=2&page_info=BAZQUUX",
+ body=json.dumps({"products": fixture[:2]}),
+ response_headers=next_headers,
+ )
+
+ def test_nonpaginates_collection(self):
+ self.fake("draft_orders", method="GET", code=200, body=self.load_fixture("draft_orders"))
+ draft_orders = shopify.DraftOrder.find()
+ self.assertEqual(1, len(draft_orders))
+ self.assertEqual(517119332, draft_orders[0].id)
+ self.assertIsInstance(
+ draft_orders, shopify.collection.PaginatedCollection, "find() result is not PaginatedCollection"
+ )
+
+ def test_paginated_collection(self):
+ items = shopify.Product.find(limit=2)
+ self.assertIsInstance(items, shopify.collection.PaginatedCollection, "find() result is not PaginatedCollection")
+ self.assertEqual(len(items), 2, "find() result has incorrect length")
+
+ def test_pagination_next_page(self):
+ c = shopify.Product.find(limit=2)
+ self.assertEqual(c.next_page_url, self.next_page_url, "next url is incorrect")
+ n = c.next_page()
+ self.assertEqual(n.previous_page_url, self.prev_page_url, "prev url is incorrect")
+ self.assertIsInstance(
+ n, shopify.collection.PaginatedCollection, "next_page() result is not PaginatedCollection"
+ )
+ self.assertEqual(len(n), 2, "next_page() collection has incorrect length")
+ self.assertIn("pagination", n.metadata)
+ self.assertIn("previous", n.metadata["pagination"], "next_page() collection doesn't have a previous page")
+
+ with self.assertRaises(IndexError, msg="next_page() did not raise with no next page"):
+ n.next_page()
+
+ def test_pagination_previous(self):
+ c = shopify.Product.find(limit=2)
+ self.assertEqual(c.next_page_url, self.next_page_url, "next url is incorrect")
+ self.assertTrue(c.has_next_page())
+ n = c.next_page()
+ self.assertEqual(n.previous_page_url, self.prev_page_url, "prev url is incorrect")
+ self.assertTrue(n.has_previous_page())
+
+ p = n.previous_page()
+
+ self.assertIsInstance(
+ p, shopify.collection.PaginatedCollection, "previous_page() result is not PaginatedCollection"
+ )
+ self.assertEqual(len(p), 4, "previous_page() collection has incorrect length") # cached
+ self.assertIn("pagination", p.metadata)
+ self.assertIn("next", p.metadata["pagination"], "previous_page() collection doesn't have a next page")
+
+ with self.assertRaises(IndexError, msg="previous_page() did not raise with no previous page"):
+ p.previous_page()
+
+ def test_paginated_collection_iterator(self):
+ c = shopify.Product.find(limit=2)
+
+ i = iter(c)
+ self.assertEqual(next(i).id, 1)
+ self.assertEqual(next(i).id, 2)
+ with self.assertRaises(StopIteration):
+ next(i)
+
+ def test_paginated_collection_no_cache(self):
+ c = shopify.Product.find(limit=2)
+
+ n = c.next_page(no_cache=True)
+ self.assertIsNone(c._next, "no_cache=True still caches")
+ self.assertIsNone(n._previous, "no_cache=True still caches")
+
+ p = n.previous_page(no_cache=True)
+ self.assertIsNone(p._next, "no_cache=True still caches")
+ self.assertIsNone(n._previous, "no_cache=True still caches")
+
+ def test_paginated_iterator(self):
+ c = shopify.Product.find(limit=2)
+
+ i = iter(shopify.PaginatedIterator(c))
+
+ first_page = iter(next(i))
+ self.assertEqual(next(first_page).id, 1)
+ self.assertEqual(next(first_page).id, 2)
+ with self.assertRaises(StopIteration):
+ next(first_page)
+
+ second_page = iter(next(i))
+ self.assertEqual(next(second_page).id, 3)
+ self.assertEqual(next(second_page).id, 4)
+ with self.assertRaises(StopIteration):
+ next(second_page)
+
+ with self.assertRaises(StopIteration):
+ next(i)
diff --git a/test/payouts_test.py b/test/payouts_test.py
new file mode 100644
index 00000000..f82851c2
--- /dev/null
+++ b/test/payouts_test.py
@@ -0,0 +1,17 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class PayoutsTest(TestCase):
+ prefix = "/admin/api/unstable/shopify_payments"
+
+ def test_get_payouts(self):
+ self.fake("payouts", method="GET", prefix=self.prefix, body=self.load_fixture("payouts"))
+ payouts = shopify.Payouts.find()
+ self.assertGreater(len(payouts), 0)
+
+ def test_get_one_payout(self):
+ self.fake("payouts/623721858", method="GET", prefix=self.prefix, body=self.load_fixture("payout"))
+ payouts = shopify.Payouts.find(623721858)
+ self.assertEqual("paid", payouts.status)
+ self.assertEqual("41.90", payouts.amount)
diff --git a/test/price_rules_test.py b/test/price_rules_test.py
new file mode 100644
index 00000000..a28b15de
--- /dev/null
+++ b/test/price_rules_test.py
@@ -0,0 +1,109 @@
+import json
+from test.test_helper import TestCase
+
+import shopify
+
+
+class PriceRuleTest(TestCase):
+ def setUp(self):
+ super(PriceRuleTest, self).setUp()
+ self.fake("price_rules/1213131", body=self.load_fixture("price_rule"))
+ self.price_rule = shopify.PriceRule.find(1213131)
+
+ def test_get_price_rule(self):
+ self.fake("price_rule/1213131", method="GET", code=200, body=self.load_fixture("price_rule"))
+ price_rule = shopify.PriceRule.find(1213131)
+ self.assertEqual(1213131, price_rule.id)
+
+ def test_get_all_price_rules(self):
+ self.fake("price_rules", method="GET", code=200, body=self.load_fixture("price_rules"))
+ price_rules = shopify.PriceRule.find()
+ self.assertEqual(2, len(price_rules))
+
+ def test_update_price_rule(self):
+ self.price_rule.title = "Buy One Get One"
+ self.fake(
+ "price_rules/1213131",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("price_rule"),
+ headers={"Content-type": "application/json"},
+ )
+ self.price_rule.save()
+ self.assertEqual("Buy One Get One", json.loads(self.http.request.data.decode("utf-8"))["price_rule"]["title"])
+
+ def test_delete_price_rule(self):
+ self.fake("price_rules/1213131", method="DELETE", body="destroyed")
+ self.price_rule.destroy()
+ self.assertEqual("DELETE", self.http.request.get_method())
+
+ def test_price_rule_creation(self):
+ self.fake(
+ "price_rules",
+ method="POST",
+ code=202,
+ body=self.load_fixture("price_rule"),
+ headers={"Content-type": "application/json"},
+ )
+ price_rule = shopify.PriceRule.create(
+ {
+ "title": "BOGO",
+ "target_type": "line_item",
+ "target_selection": "all",
+ "allocation_method": "across",
+ "value_type": "percentage",
+ "value": -100,
+ "once_per_customer": "true",
+ "customer_selection": "all",
+ }
+ )
+ self.assertEqual("BOGO", price_rule.title)
+ self.assertEqual("line_item", price_rule.target_type)
+
+ def test_get_discount_codes(self):
+ self.fake(
+ "price_rules/1213131/discount_codes", method="GET", code=200, body=self.load_fixture("discount_codes")
+ )
+ discount_codes = self.price_rule.discount_codes()
+ self.assertEqual(1, len(discount_codes))
+
+ def test_add_discount_code(self):
+ price_rule_discount_fixture = self.load_fixture("discount_code")
+ discount_code = json.loads(price_rule_discount_fixture.decode("utf-8"))
+ self.fake(
+ "price_rules/1213131/discount_codes",
+ method="POST",
+ body=price_rule_discount_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ price_rule_discount_response = self.price_rule.add_discount_code(
+ shopify.DiscountCode(discount_code["discount_code"])
+ )
+ self.assertEqual(discount_code, json.loads(self.http.request.data.decode("utf-8")))
+ self.assertIsInstance(price_rule_discount_response, shopify.DiscountCode)
+ self.assertEqual(discount_code["discount_code"]["code"], price_rule_discount_response.code)
+
+ def test_create_batch_discount_codes(self):
+ self.fake(
+ "price_rules/1213131/batch",
+ method="POST",
+ code=201,
+ body=self.load_fixture("discount_code_creation"),
+ headers={"Content-type": "application/json"},
+ )
+ batch = self.price_rule.create_batch([{"code": "SUMMER1"}, {"code": "SUMMER2"}, {"code": "SUMMER3"}])
+
+ self.assertEqual(3, batch.codes_count)
+ self.assertEqual("queued", batch.status)
+
+ def test_find_batch_job(self):
+ self.fake(
+ "price_rules/1213131/batch/989355119",
+ method="GET",
+ code=200,
+ body=self.load_fixture("discount_code_creation"),
+ )
+ batch = self.price_rule.find_batch(989355119)
+
+ self.assertEqual(3, batch.codes_count)
+ self.assertEqual("queued", batch.status)
diff --git a/test/product_listing_test.py b/test/product_listing_test.py
new file mode 100644
index 00000000..dcdc048d
--- /dev/null
+++ b/test/product_listing_test.py
@@ -0,0 +1,43 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class ProductListingTest(TestCase):
+ def test_get_product_listings(self):
+ self.fake("product_listings", method="GET", code=200, body=self.load_fixture("product_listings"))
+
+ product_listings = shopify.ProductListing.find()
+ self.assertEqual(2, len(product_listings))
+ self.assertEqual(2, product_listings[0].product_id)
+ self.assertEqual(1, product_listings[1].product_id)
+ self.assertEqual("Synergistic Silk Chair", product_listings[0].title)
+ self.assertEqual("Rustic Copper Bottle", product_listings[1].title)
+
+ def test_get_product_listing(self):
+ self.fake("product_listings/2", method="GET", code=200, body=self.load_fixture("product_listing"))
+
+ product_listing = shopify.ProductListing.find(2)
+ self.assertEqual("Synergistic Silk Chair", product_listing.title)
+
+ def test_reload_product_listing(self):
+ self.fake("product_listings/2", method="GET", code=200, body=self.load_fixture("product_listing"))
+
+ product_listing = shopify.ProductListing()
+ product_listing.product_id = 2
+ product_listing.reload()
+
+ self.assertEqual("Synergistic Silk Chair", product_listing.title)
+
+ def test_get_product_listing_product_ids(self):
+ self.fake(
+ "product_listings/product_ids",
+ method="GET",
+ status=200,
+ body=self.load_fixture("product_listing_product_ids"),
+ )
+
+ product_ids = shopify.ProductListing.product_ids()
+
+ self.assertEqual(2, len(product_ids))
+ self.assertEqual(2, product_ids[0])
+ self.assertEqual(1, product_ids[1])
diff --git a/test/product_publication_test.py b/test/product_publication_test.py
new file mode 100644
index 00000000..671e7786
--- /dev/null
+++ b/test/product_publication_test.py
@@ -0,0 +1,68 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class ProductPublicationTest(TestCase):
+ def test_find_all_product_publications(self):
+ self.fake(
+ "publications/55650051/product_publications", method="GET", body=self.load_fixture("product_publications")
+ )
+ product_publications = shopify.ProductPublication.find(publication_id=55650051)
+
+ self.assertEqual(647162527768, product_publications[0].id)
+ self.assertEqual(8267093571, product_publications[0].product_id)
+
+ def test_find_product_publication(self):
+ self.fake(
+ "publications/55650051/product_publications/647162527768",
+ method="GET",
+ body=self.load_fixture("product_publication"),
+ code=200,
+ )
+ product_publication = shopify.ProductPublication.find(647162527768, publication_id=55650051)
+
+ self.assertEqual(647162527768, product_publication.id)
+ self.assertEqual(8267093571, product_publication.product_id)
+
+ def test_create_product_publication(self):
+ self.fake(
+ "publications/55650051/product_publications",
+ method="POST",
+ headers={"Content-type": "application/json"},
+ body=self.load_fixture("product_publication"),
+ code=201,
+ )
+
+ product_publication = shopify.ProductPublication.create(
+ {
+ "publication_id": 55650051,
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "product_id": 8267093571,
+ }
+ )
+
+ expected_body = {
+ "product_publication": {
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "product_id": 8267093571,
+ }
+ }
+
+ self.assertEqual(expected_body, json.loads(self.http.request.data.decode("utf-8")))
+
+ def test_destroy_product_publication(self):
+ self.fake(
+ "publications/55650051/product_publications/647162527768",
+ method="GET",
+ body=self.load_fixture("product_publication"),
+ code=200,
+ )
+ product_publication = shopify.ProductPublication.find(647162527768, publication_id=55650051)
+
+ self.fake("publications/55650051/product_publications/647162527768", method="DELETE", body="{}", code=200)
+ product_publication.destroy()
+
+ self.assertEqual("DELETE", self.http.request.get_method())
diff --git a/test/product_test.py b/test/product_test.py
new file mode 100644
index 00000000..de183691
--- /dev/null
+++ b/test/product_test.py
@@ -0,0 +1,87 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class ProductTest(TestCase):
+ def setUp(self):
+ super(ProductTest, self).setUp()
+
+ self.fake("products/632910392", body=self.load_fixture("product"))
+ self.product = shopify.Product.find(632910392)
+
+ def test_add_metafields_to_product(self):
+ self.fake(
+ "products/632910392/metafields",
+ method="POST",
+ code=201,
+ body=self.load_fixture("metafield"),
+ headers={"Content-type": "application/json"},
+ )
+
+ field = self.product.add_metafield(
+ shopify.Metafield(
+ {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"}
+ )
+ )
+
+ self.assertFalse(field.is_new())
+ self.assertEqual("contact", field.namespace)
+ self.assertEqual("email", field.key)
+ self.assertEqual("123@example.com", field.value)
+
+ def test_get_metafields_for_product(self):
+ self.fake("products/632910392/metafields", body=self.load_fixture("metafields"))
+
+ metafields = self.product.metafields()
+
+ self.assertEqual(2, len(metafields))
+ for field in metafields:
+ self.assertTrue(isinstance(field, shopify.Metafield))
+
+ def test_get_metafields_for_product_with_params(self):
+ self.fake("products/632910392/metafields.json?limit=2", extension=False, body=self.load_fixture("metafields"))
+
+ metafields = self.product.metafields(limit=2)
+ self.assertEqual(2, len(metafields))
+ for field in metafields:
+ self.assertTrue(isinstance(field, shopify.Metafield))
+
+ def test_get_metafields_for_product_count(self):
+ self.fake("products/632910392/metafields/count", body=self.load_fixture("metafields_count"))
+
+ metafields_count = self.product.metafields_count()
+ self.assertEqual(2, metafields_count)
+
+ def test_get_metafields_for_product_count_with_params(self):
+ self.fake(
+ "products/632910392/metafields/count.json?value_type=string",
+ extension=False,
+ body=self.load_fixture("metafields_count"),
+ )
+
+ metafields_count = self.product.metafields_count(value_type="string")
+ self.assertEqual(2, metafields_count)
+
+ def test_update_loaded_variant(self):
+ self.fake("products/632910392/variants/808950810", method="PUT", code=200, body=self.load_fixture("variant"))
+
+ variant = self.product.variants[0]
+ variant.price = "0.50"
+ variant.save
+
+ def test_add_variant_to_product(self):
+ self.fake(
+ "products/632910392/variants",
+ method="POST",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
+ self.fake(
+ "products/632910392/variants/808950810",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
+ v = shopify.Variant()
+ self.assertTrue(self.product.add_variant(v))
diff --git a/test/publication_test.py b/test/publication_test.py
new file mode 100644
index 00000000..dab26fc5
--- /dev/null
+++ b/test/publication_test.py
@@ -0,0 +1,11 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class PublicationTest(TestCase):
+ def test_find_all_publications(self):
+ self.fake("publications")
+ publications = shopify.Publication.find()
+
+ self.assertEqual(55650051, publications[0].id)
+ self.assertEqual("Buy Button", publications[0].name)
diff --git a/test/recurring_charge_test.py b/test/recurring_charge_test.py
new file mode 100644
index 00000000..c785388e
--- /dev/null
+++ b/test/recurring_charge_test.py
@@ -0,0 +1,63 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class RecurringApplicationChargeTest(TestCase):
+ def test_activate_charge(self):
+ # Just check that calling activate doesn't raise an exception.
+ self.fake(
+ "recurring_application_charges/35463/activate",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=" ",
+ )
+ charge = shopify.RecurringApplicationCharge({"id": 35463})
+ charge.activate()
+
+ def test_current_method_returns_active_charge(self):
+ # Test that current() class method correctly returns
+ # first RecurringApplicationCharge with active status
+ self.fake("recurring_application_charges")
+ charge = shopify.RecurringApplicationCharge.current()
+ self.assertEqual(charge.id, 455696195)
+
+ def test_current_method_returns_none_if_active_not_found(self):
+ # Test that current() class method correctly returns
+ # None if RecurringApplicationCharge with active status not found
+ self.fake("recurring_application_charges", body=self.load_fixture("recurring_application_charges_no_active"))
+ charge = shopify.RecurringApplicationCharge.current()
+ self.assertEqual(charge, None)
+
+ def test_usage_charges_method_returns_associated_usage_charges(self):
+ self.fake("recurring_application_charges")
+ charge = shopify.RecurringApplicationCharge.current()
+
+ self.fake(
+ "recurring_application_charges/455696195/usage_charges",
+ method="GET",
+ body=self.load_fixture("usage_charges"),
+ )
+ usage_charges = charge.usage_charges()
+ self.assertEqual(len(usage_charges), 2)
+
+ def test_customize_method_increases_capped_amount(self):
+ self.fake("recurring_application_charges")
+ charge = shopify.RecurringApplicationCharge.current()
+ self.assertEqual(charge.capped_amount, 100)
+
+ self.fake(
+ "recurring_application_charges/455696195/customize.json?recurring_application_charge%5Bcapped_amount%5D=200",
+ extension=False,
+ method="PUT",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=self.load_fixture("recurring_application_charge_adjustment"),
+ )
+ charge.customize(capped_amount=200)
+ self.assertTrue(charge.update_capped_amount_url)
+
+ def test_destroy_recurring_application_charge(self):
+ self.fake("recurring_application_charges")
+ charge = shopify.RecurringApplicationCharge.current()
+
+ self.fake("recurring_application_charges/455696195", method="DELETE", body="{}")
+ charge.destroy()
diff --git a/test/refund_test.py b/test/refund_test.py
new file mode 100644
index 00000000..905bbabc
--- /dev/null
+++ b/test/refund_test.py
@@ -0,0 +1,28 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class RefundTest(TestCase):
+ def setUp(self):
+ super(RefundTest, self).setUp()
+ self.fake("orders/450789469/refunds/509562969", method="GET", body=self.load_fixture("refund"))
+
+ def test_should_find_a_specific_refund(self):
+ refund = shopify.Refund.find(509562969, order_id=450789469)
+ self.assertEqual("209.00", refund.transactions[0].amount)
+
+ def test_calculate_refund_for_order(self):
+ self.fake(
+ "orders/450789469/refunds/calculate",
+ method="POST",
+ code=201,
+ body=self.load_fixture("refund_calculate"),
+ headers={"Content-type": "application/json"},
+ )
+ refund = shopify.Refund.calculate(
+ order_id=450789469, refund_line_items=[{"line_item_id": 518995019, "quantity": 1}]
+ )
+
+ self.assertEqual("suggested_refund", refund.transactions[0].kind)
+ self.assertEqual("41.94", refund.transactions[0].amount)
+ self.assertEqual(518995019, refund.refund_line_items[0].line_item_id)
diff --git a/test/report_test.py b/test/report_test.py
new file mode 100644
index 00000000..bfb66935
--- /dev/null
+++ b/test/report_test.py
@@ -0,0 +1,36 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class CustomerSavedSearchTest(TestCase):
+ def test_get_report(self):
+ self.fake("reports/987", method="GET", code=200, body=self.load_fixture("report"))
+ report = shopify.Report.find(987)
+ self.assertEqual(987, report.id)
+
+ def test_get_reports(self):
+ self.fake("reports", method="GET", code=200, body=self.load_fixture("reports"))
+ reports = shopify.Report.find()
+ self.assertEqual("custom_app_reports", reports[0].category)
+
+ def test_create_report(self):
+ self.fake(
+ "reports",
+ method="POST",
+ code=201,
+ body=self.load_fixture("report"),
+ headers={"Content-type": "application/json"},
+ )
+ report = shopify.Report.create(
+ {
+ "name": "Custom App Report",
+ "shopify_ql": "SHOW quantity_count, total_sales BY product_type, vendor, product_title FROM products SINCE -1m UNTIL -0m ORDER BY total_sales DESC",
+ }
+ )
+ self.assertEqual("custom_app_reports", report.category)
+
+ def test_delete_report(self):
+ self.fake("reports/987", method="GET", code=200, body=self.load_fixture("report"))
+ self.fake("reports", method="DELETE", code=200, body="[]")
+ report = shopify.Report.find(987)
+ self.assertTrue(report.destroy)
diff --git a/test/resource_feedback_test.py b/test/resource_feedback_test.py
new file mode 100644
index 00000000..47ee9f92
--- /dev/null
+++ b/test/resource_feedback_test.py
@@ -0,0 +1,40 @@
+import json
+import shopify
+from test.test_helper import TestCase
+
+
+class ResourceFeedbackTest(TestCase):
+ def test_get_resource_feedback(self):
+ body = json.dumps({"resource_feedback": [{"resource_type": "Shop"}]})
+ self.fake("resource_feedback", method="GET", body=body)
+
+ feedback = shopify.ResourceFeedback.find()
+
+ self.assertEqual("Shop", feedback[0].resource_type)
+
+ def test_save_with_resource_feedback_endpoint(self):
+ body = json.dumps({"resource_feedback": {}})
+ self.fake("resource_feedback", method="POST", body=body, headers={"Content-Type": "application/json"})
+
+ shopify.ResourceFeedback().save()
+
+ self.assertEqual(body, self.http.request.data.decode("utf-8"))
+
+ def test_get_resource_feedback_with_product_id(self):
+ body = json.dumps({"resource_feedback": [{"resource_type": "Product"}]})
+ self.fake("products/42/resource_feedback", method="GET", body=body)
+
+ feedback = shopify.ResourceFeedback.find(product_id=42)
+
+ self.assertEqual("Product", feedback[0].resource_type)
+
+ def test_save_with_product_id_resource_feedback_endpoint(self):
+ body = json.dumps({"resource_feedback": {}})
+ self.fake(
+ "products/42/resource_feedback", method="POST", body=body, headers={"Content-Type": "application/json"}
+ )
+
+ feedback = shopify.ResourceFeedback({"product_id": 42})
+ feedback.save()
+
+ self.assertEqual(body, self.http.request.data.decode("utf-8"))
diff --git a/test/session_test.py b/test/session_test.py
new file mode 100644
index 00000000..8d73e293
--- /dev/null
+++ b/test/session_test.py
@@ -0,0 +1,321 @@
+import shopify
+from test.test_helper import TestCase
+import hmac
+from hashlib import sha256
+import time
+from six.moves import urllib
+from six import u
+
+
+class SessionTest(TestCase):
+ @classmethod
+ def setUpClass(self):
+ shopify.ApiVersion.define_known_versions()
+ shopify.ApiVersion.define_version(shopify.Release("2019-04"))
+
+ @classmethod
+ def tearDownClass(self):
+ shopify.ApiVersion.clear_defined_versions()
+
+ def test_not_be_valid_without_a_url(self):
+ session = shopify.Session("", "unstable", "any-token")
+ self.assertFalse(session.valid)
+
+ def test_not_be_valid_without_token(self):
+ session = shopify.Session("testshop.myshopify.com", "unstable")
+ self.assertFalse(session.valid)
+
+ def test_be_valid_with_any_token_and_any_url(self):
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
+ self.assertTrue(session.valid)
+
+ def test_ignore_everything_but_the_subdomain_in_the_shop(self):
+ session = shopify.Session("http://user:pass@testshop.notshopify.net/path", "unstable", "any-token")
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", session.site)
+
+ def test_append_the_myshopify_domain_if_not_given(self):
+ session = shopify.Session("testshop", "unstable", "any-token")
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", session.site)
+
+ def test_raise_error_if_params_passed_but_signature_omitted(self):
+ with self.assertRaises(shopify.ValidationException):
+ session = shopify.Session("testshop.myshopify.com", "unstable")
+ token = session.request_token({"code": "any_code", "foo": "bar", "timestamp": "1234"})
+
+ def test_setup_api_key_and_secret_for_all_sessions(self):
+ shopify.Session.setup(api_key="My test key", secret="My test secret")
+ self.assertEqual("My test key", shopify.Session.api_key)
+ self.assertEqual("My test secret", shopify.Session.secret)
+
+ def test_use_https_protocol_by_default_for_all_sessions(self):
+ self.assertEqual("https", shopify.Session.protocol)
+
+ def test_temp_reset_shopify_shopify_resource_site_to_original_value(self):
+ shopify.Session.setup(api_key="key", secret="secret")
+ session1 = shopify.Session("fakeshop.myshopify.com", "2019-04", "token1")
+ shopify.ShopifyResource.activate_session(session1)
+
+ assigned_site = ""
+ with shopify.Session.temp("testshop.myshopify.com", "unstable", "any-token"):
+ assigned_site = shopify.ShopifyResource.site
+
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", assigned_site)
+ self.assertEqual("https://fakeshop.myshopify.com/admin/api/2019-04", shopify.ShopifyResource.site)
+
+ def test_myshopify_domain_supports_non_standard_ports(self):
+ try:
+ shopify.Session.setup(api_key="key", secret="secret", myshopify_domain="localhost", port=3000)
+
+ session = shopify.Session("fakeshop.localhost:3000", "unstable", "token1")
+ shopify.ShopifyResource.activate_session(session)
+ self.assertEqual("https://fakeshop.localhost:3000/admin/api/unstable", shopify.ShopifyResource.site)
+
+ session = shopify.Session("fakeshop", "unstable", "token1")
+ shopify.ShopifyResource.activate_session(session)
+ self.assertEqual("https://fakeshop.localhost:3000/admin/api/unstable", shopify.ShopifyResource.site)
+ finally:
+ shopify.Session.setup(myshopify_domain="myshopify.com", port=None)
+
+ def test_temp_works_without_currently_active_session(self):
+ shopify.ShopifyResource.clear_session()
+
+ assigned_site = ""
+ with shopify.Session.temp("testshop.myshopify.com", "unstable", "any-token"):
+ assigned_site = shopify.ShopifyResource.site
+
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", assigned_site)
+ self.assertEqual("https://none/admin/api/unstable", shopify.ShopifyResource.site)
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ permission_url = session.create_permission_url("my_redirect_uri.com")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_scope(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ scope = ["write_products"]
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_dual_scope(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ scope = ["write_products", "write_customers"]
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products%2Cwrite_customers",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_empty_scope(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ scope = []
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_state(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ permission_url = session.create_permission_url("my_redirect_uri.com", state="mystate")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&state=mystate",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_empty_scope_and_state(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ scope = []
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope, state="mystate")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&state=mystate",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_scope_and_state(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ scope = ["write_customers"]
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope, state="mystate")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_customers&state=mystate",
+ self.normalize_url(permission_url),
+ )
+
+ def test_raise_exception_if_code_invalid_in_request_token(self):
+ shopify.Session.setup(api_key="My test key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ self.fake(
+ None,
+ url="https://localhost.myshopify.com/admin/oauth/access_token",
+ method="POST",
+ code=404,
+ body='{"error" : "invalid_request"}',
+ has_user_agent=False,
+ )
+
+ with self.assertRaises(shopify.ValidationException):
+ session.request_token({"code": "any-code", "timestamp": "1234"})
+
+ self.assertFalse(session.valid)
+
+ def test_return_site_for_session(self):
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", session.site)
+
+ def test_hmac_calculation(self):
+ # Test using the secret and parameter examples given in the Shopify API documentation.
+ shopify.Session.secret = "hush"
+ params = {
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "timestamp": "1337178173",
+ "hmac": "2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2",
+ }
+ self.assertEqual(shopify.Session.calculate_hmac(params), params["hmac"])
+
+ def test_hmac_calculation_with_ampersand_and_equal_sign_characters(self):
+ shopify.Session.secret = "secret"
+ params = {"a": "1&b=2", "c=3&d": "4"}
+ to_sign = "a=1%26b=2&c%3D3%26d=4"
+ expected_hmac = hmac.new("secret".encode(), to_sign.encode(), sha256).hexdigest()
+ self.assertEqual(shopify.Session.calculate_hmac(params), expected_hmac)
+
+ def test_hmac_validation(self):
+ # Test using the secret and parameter examples given in the Shopify API documentation.
+ shopify.Session.secret = "hush"
+ params = {
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "timestamp": "1337178173",
+ "hmac": u("2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2"),
+ }
+ self.assertTrue(shopify.Session.validate_hmac(params))
+
+ def test_parameter_validation_handles_missing_params(self):
+ # Test using the secret and parameter examples given in the Shopify API documentation.
+ shopify.Session.secret = "hush"
+ params = {
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "hmac": u("2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2"),
+ }
+ self.assertFalse(shopify.Session.validate_params(params))
+
+ def test_param_validation_of_param_values_with_lists(self):
+ shopify.Session.secret = "hush"
+ params = {
+ "shop": "some-shop.myshopify.com",
+ "ids[]": [
+ 2,
+ 1,
+ ],
+ "hmac": u("b93b9f82996f6f8bf9f1b7bbddec284c8fabacdc4e12dc80550b4705f3003b1e"),
+ }
+ self.assertEqual(True, shopify.Session.validate_hmac(params))
+
+ def test_return_token_and_scope_if_hmac_is_valid(self):
+ shopify.Session.secret = "secret"
+ params = {"code": "any-code", "timestamp": time.time()}
+ hmac = shopify.Session.calculate_hmac(params)
+ params["hmac"] = hmac
+
+ self.fake(
+ None,
+ url="https://localhost.myshopify.com/admin/oauth/access_token",
+ method="POST",
+ body='{"access_token" : "token", "scope": "read_products,write_orders"}',
+ has_user_agent=False,
+ )
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ token = session.request_token(params)
+ self.assertEqual("token", token)
+ self.assertEqual(shopify.ApiAccess("read_products,write_orders"), session.access_scopes)
+
+ def test_raise_error_if_hmac_is_invalid(self):
+ shopify.Session.secret = "secret"
+ params = {"code": "any-code", "timestamp": time.time()}
+ params["hmac"] = "a94a110d86d2452e92a4a64275b128e9273be3037f2c339eb3e2af4cfb8a3828"
+
+ with self.assertRaises(shopify.ValidationException):
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ session = session.request_token(params)
+
+ def test_raise_error_if_hmac_does_not_match_expected(self):
+ shopify.Session.secret = "secret"
+ params = {"foo": "hello", "timestamp": time.time()}
+ hmac = shopify.Session.calculate_hmac(params)
+ params["hmac"] = hmac
+ params["bar"] = "world"
+ params["code"] = "code"
+
+ with self.assertRaises(shopify.ValidationException):
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ session = session.request_token(params)
+
+ def test_raise_error_if_timestamp_is_too_old(self):
+ shopify.Session.secret = "secret"
+ one_day = 24 * 60 * 60
+ params = {"code": "any-code", "timestamp": time.time() - (2 * one_day)}
+ hmac = shopify.Session.calculate_hmac(params)
+ params["hmac"] = hmac
+
+ with self.assertRaises(shopify.ValidationException):
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ session = session.request_token(params)
+
+ def test_access_scopes_are_nil_by_default(self):
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
+ self.assertIsNone(session.access_scopes)
+
+ def test_access_scopes_when_valid_scopes_passed_in(self):
+ session = shopify.Session(
+ shop_url="testshop.myshopify.com",
+ version="unstable",
+ token="any-token",
+ access_scopes="read_products, write_orders",
+ )
+
+ expected_access_scopes = shopify.ApiAccess("read_products, write_orders")
+ self.assertEqual(expected_access_scopes, session.access_scopes)
+
+ def test_access_scopes_set_with_api_access_object_passed_in(self):
+ session = shopify.Session(
+ shop_url="testshop.myshopify.com",
+ version="unstable",
+ token="any-token",
+ access_scopes=shopify.ApiAccess("read_products, write_orders"),
+ )
+
+ expected_access_scopes = shopify.ApiAccess("read_products, write_orders")
+ self.assertEqual(expected_access_scopes, session.access_scopes)
+
+ def normalize_url(self, url):
+ scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
+ query = "&".join(sorted(query.split("&")))
+ return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
+
+ def test_session_with_coerced_version(self):
+ future_version = "2030-01"
+ session = shopify.Session("test.myshopify.com", future_version, "token")
+ self.assertEqual(session.api_version.name, future_version)
+ self.assertEqual(
+ session.api_version.api_path("https://test.myshopify.com"),
+ f"https://test.myshopify.com/admin/api/{future_version}",
+ )
+
+ def test_session_with_invalid_version(self):
+ with self.assertRaises(shopify.VersionNotFoundError):
+ shopify.Session("test.myshopify.com", "invalid-version", "token")
diff --git a/test/session_token_test.py b/test/session_token_test.py
new file mode 100644
index 00000000..0df7147f
--- /dev/null
+++ b/test/session_token_test.py
@@ -0,0 +1,110 @@
+from shopify import session_token
+from test.test_helper import TestCase
+from datetime import datetime, timedelta
+
+import jwt
+import sys
+
+if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0
+ import time
+
+
+def timestamp(date):
+ return time.mktime(date.timetuple()) if sys.version_info[0] < 3 else date.timestamp()
+
+
+class TestSessionTokenGetDecodedSessionToken(TestCase):
+ @classmethod
+ def setUpClass(self):
+ self.secret = "API Secret"
+ self.api_key = "API key"
+
+ @classmethod
+ def setUp(self):
+ current_time = datetime.now()
+ self.payload = {
+ "iss": "https://test-shop.myshopify.com/admin",
+ "dest": "https://test-shop.myshopify.com",
+ "aud": self.api_key,
+ "sub": "1",
+ "exp": timestamp((current_time + timedelta(0, 60))),
+ "nbf": timestamp(current_time),
+ "iat": timestamp(current_time),
+ "jti": "4321",
+ "sid": "abc123",
+ }
+
+ @classmethod
+ def build_auth_header(self):
+ mock_session_token = jwt.encode(self.payload, self.secret, algorithm="HS256")
+ return "Bearer {session_token}".format(session_token=mock_session_token)
+
+ def test_raises_if_token_authentication_header_is_not_bearer(self):
+ authorization_header = "Bad auth header"
+
+ with self.assertRaises(session_token.TokenAuthenticationError) as cm:
+ session_token.decode_from_header(authorization_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token", str(cm.exception))
+
+ def test_raises_jwt_error_if_session_token_is_expired(self):
+ self.payload["exp"] = timestamp((datetime.now() + timedelta(0, -11)))
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Signature has expired", str(cm.exception))
+
+ def test_raises_jwt_error_if_invalid_alg(self):
+ bad_session_token = jwt.encode(self.payload, None, algorithm="none")
+ invalid_header = "Bearer {session_token}".format(session_token=bad_session_token)
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(invalid_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The specified alg value is not allowed", str(cm.exception))
+
+ def test_raises_jwt_error_if_invalid_signature(self):
+ bad_session_token = jwt.encode(self.payload, "bad_secret", algorithm="HS256")
+ invalid_header = "Bearer {session_token}".format(session_token=bad_session_token)
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(invalid_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Signature verification failed", str(cm.exception))
+
+ def test_raises_if_aud_doesnt_match_api_key(self):
+ self.payload["aud"] = "bad audience"
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Audience doesn't match", str(cm.exception))
+
+ def test_raises_if_issuer_hostname_is_invalid(self):
+ self.payload["iss"] = "bad_shop_hostname"
+
+ with self.assertRaises(session_token.InvalidIssuerError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Invalid issuer", str(cm.exception))
+
+ def test_raises_if_iss_and_dest_dont_match(self):
+ self.payload["dest"] = "bad_shop.myshopify.com"
+
+ with self.assertRaises(session_token.MismatchedHostsError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The issuer and destination do not match", str(cm.exception))
+
+ def test_returns_decoded_payload(self):
+ decoded_payload = session_token.decode_from_header(
+ self.build_auth_header(), api_key=self.api_key, secret=self.secret
+ )
+
+ self.assertEqual(self.payload, decoded_payload)
+
+ def test_allow_10_seconds_clock_skew_in_nbf(self):
+ self.payload["nbf"] = timestamp((datetime.now() + timedelta(seconds=10)))
+
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
diff --git a/test/shipping_zone_test.py b/test/shipping_zone_test.py
new file mode 100644
index 00000000..3d1e1e4d
--- /dev/null
+++ b/test/shipping_zone_test.py
@@ -0,0 +1,11 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class ShippingZoneTest(TestCase):
+ def test_get_shipping_zones(self):
+ self.fake("shipping_zones", method="GET", body=self.load_fixture("shipping_zones"))
+ shipping_zones = shopify.ShippingZone.find()
+ self.assertEqual(1, len(shipping_zones))
+ self.assertEqual(shipping_zones[0].name, "Some zone")
+ self.assertEqual(3, len(shipping_zones[0].countries))
diff --git a/test/shop_test.py b/test/shop_test.py
new file mode 100644
index 00000000..3a88a2a6
--- /dev/null
+++ b/test/shop_test.py
@@ -0,0 +1,55 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class ShopTest(TestCase):
+ def setUp(self):
+ super(ShopTest, self).setUp()
+ self.fake("shop")
+ self.shop = shopify.Shop.current()
+
+ def test_current_should_return_current_shop(self):
+ self.assertTrue(isinstance(self.shop, shopify.Shop))
+ self.assertEqual("Apple Computers", self.shop.name)
+ self.assertEqual("apple.myshopify.com", self.shop.myshopify_domain)
+ self.assertEqual(690933842, self.shop.id)
+ self.assertEqual("2007-12-31T19:00:00-05:00", self.shop.created_at)
+ self.assertIsNone(self.shop.tax_shipping)
+
+ def test_get_metafields_for_shop(self):
+ self.fake("metafields")
+
+ metafields = self.shop.metafields()
+
+ self.assertEqual(2, len(metafields))
+ for field in metafields:
+ self.assertTrue(isinstance(field, shopify.Metafield))
+
+ def test_add_metafield(self):
+ self.fake(
+ "metafields",
+ method="POST",
+ code=201,
+ body=self.load_fixture("metafield"),
+ headers={"Content-type": "application/json"},
+ )
+
+ field = self.shop.add_metafield(
+ shopify.Metafield(
+ {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"}
+ )
+ )
+
+ self.assertFalse(field.is_new())
+ self.assertEqual("contact", field.namespace)
+ self.assertEqual("email", field.key)
+ self.assertEqual("123@example.com", field.value)
+
+ def test_events(self):
+ self.fake("events")
+
+ events = self.shop.events()
+
+ self.assertEqual(3, len(events))
+ for event in events:
+ self.assertTrue(isinstance(event, shopify.Event))
diff --git a/test/storefront_access_token_test.py b/test/storefront_access_token_test.py
new file mode 100644
index 00000000..ce5ef805
--- /dev/null
+++ b/test/storefront_access_token_test.py
@@ -0,0 +1,37 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class StorefrontAccessTokenTest(TestCase):
+ def test_create_storefront_access_token(self):
+ self.fake(
+ "storefront_access_tokens",
+ method="POST",
+ body=self.load_fixture("storefront_access_token"),
+ headers={"Content-type": "application/json"},
+ )
+ storefront_access_token = shopify.StorefrontAccessToken.create({"title": "Test"})
+ self.assertEqual(1, storefront_access_token.id)
+ self.assertEqual("Test", storefront_access_token.title)
+
+ def test_get_and_delete_storefront_access_token(self):
+ self.fake(
+ "storefront_access_tokens/1", method="GET", code=200, body=self.load_fixture("storefront_access_token")
+ )
+ storefront_access_token = shopify.StorefrontAccessToken.find(1)
+
+ self.fake("storefront_access_tokens/1", method="DELETE", code=200, body="destroyed")
+ storefront_access_token.destroy()
+ self.assertEqual("DELETE", self.http.request.get_method())
+
+ def test_get_storefront_access_tokens(self):
+ self.fake(
+ "storefront_access_tokens", method="GET", code=200, body=self.load_fixture("storefront_access_tokens")
+ )
+ tokens = shopify.StorefrontAccessToken.find()
+
+ self.assertEqual(2, len(tokens))
+ self.assertEqual(1, tokens[0].id)
+ self.assertEqual(2, tokens[1].id)
+ self.assertEqual("Test 1", tokens[0].title)
+ self.assertEqual("Test 2", tokens[1].title)
diff --git a/test/tender_transaction_test.py b/test/tender_transaction_test.py
new file mode 100644
index 00000000..fe73c633
--- /dev/null
+++ b/test/tender_transaction_test.py
@@ -0,0 +1,13 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class TenderTransactionTest(TestCase):
+ def setUp(self):
+ super(TenderTransactionTest, self).setUp()
+ self.fake("tender_transactions", method="GET", body=self.load_fixture("tender_transactions"))
+
+ def test_should_load_all_tender_transactions(self):
+ tender_transactions = shopify.TenderTransaction.find()
+ self.assertEqual(3, len(tender_transactions))
+ self.assertEqual([1, 2, 3], list(map(lambda t: t.id, tender_transactions)))
diff --git a/test/test_helper.py b/test/test_helper.py
new file mode 100644
index 00000000..666ac792
--- /dev/null
+++ b/test/test_helper.py
@@ -0,0 +1,59 @@
+import os
+import sys
+import unittest
+from pyactiveresource.activeresource import ActiveResource
+from pyactiveresource.testing import http_fake
+import shopify
+
+
+class TestCase(unittest.TestCase):
+ def setUp(self):
+ ActiveResource.site = None
+ ActiveResource.headers = None
+
+ shopify.ShopifyResource.clear_session()
+ shopify.ShopifyResource.site = "https://this-is-my-test-show.myshopify.com/admin/api/unstable"
+ shopify.ShopifyResource.password = None
+ shopify.ShopifyResource.user = None
+
+ http_fake.initialize()
+ self.http = http_fake.TestHandler
+ self.http.set_response(Exception("Bad request"))
+ self.http.site = "https://this-is-my-test-show.myshopify.com"
+
+ def load_fixture(self, name, format="json"):
+ with open(os.path.dirname(__file__) + "/fixtures/%s.%s" % (name, format), "rb") as f:
+ return f.read()
+
+ def fake(self, endpoint, **kwargs):
+ body = kwargs.pop("body", None) or self.load_fixture(endpoint)
+ format = kwargs.pop("format", "json")
+ method = kwargs.pop("method", "GET")
+ prefix = kwargs.pop("prefix", "/admin/api/unstable")
+
+ if "extension" in kwargs and not kwargs["extension"]:
+ extension = ""
+ else:
+ extension = ".%s" % (kwargs.pop("extension", "json"))
+
+ url = "https://this-is-my-test-show.myshopify.com%s/%s%s" % (prefix, endpoint, extension)
+ try:
+ url = kwargs["url"]
+ except KeyError:
+ pass
+
+ headers = {}
+ if kwargs.pop("has_user_agent", True):
+ userAgent = "ShopifyPythonAPI/%s Python/%s" % (shopify.VERSION, sys.version.split(" ", 1)[0])
+ headers["User-agent"] = userAgent
+
+ try:
+ headers.update(kwargs["headers"])
+ except KeyError:
+ pass
+
+ code = kwargs.pop("code", 200)
+
+ self.http.respond_to(
+ method, url, headers, body=body, code=code, response_headers=kwargs.pop("response_headers", None)
+ )
diff --git a/test/transaction_test.py b/test/transaction_test.py
new file mode 100644
index 00000000..e02fae00
--- /dev/null
+++ b/test/transaction_test.py
@@ -0,0 +1,12 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class TransactionTest(TestCase):
+ def setUp(self):
+ super(TransactionTest, self).setUp()
+ self.fake("orders/450789469/transactions/389404469", method="GET", body=self.load_fixture("transaction"))
+
+ def test_should_find_a_specific_transaction(self):
+ transaction = shopify.Transaction.find(389404469, order_id=450789469)
+ self.assertEqual("409.94", transaction.amount)
diff --git a/test/transactions_test.py b/test/transactions_test.py
new file mode 100644
index 00000000..9b0d54b7
--- /dev/null
+++ b/test/transactions_test.py
@@ -0,0 +1,11 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class TransactionsTest(TestCase):
+ prefix = "/admin/api/unstable/shopify_payments/balance"
+
+ def test_get_payouts_transactions(self):
+ self.fake("transactions", method="GET", prefix=self.prefix, body=self.load_fixture("payouts_transactions"))
+ transactions = shopify.Transactions.find()
+ self.assertGreater(len(transactions), 0)
diff --git a/test/usage_charge_test.py b/test/usage_charge_test.py
new file mode 100644
index 00000000..a816636b
--- /dev/null
+++ b/test/usage_charge_test.py
@@ -0,0 +1,28 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class UsageChargeTest(TestCase):
+ def test_create_usage_charge(self):
+ self.fake(
+ "recurring_application_charges/654381177/usage_charges",
+ method="POST",
+ body=self.load_fixture("usage_charge"),
+ headers={"Content-type": "application/json"},
+ )
+
+ charge = shopify.UsageCharge(
+ {"price": 9.0, "description": "1000 emails", "recurring_application_charge_id": 654381177}
+ )
+ charge.save()
+ self.assertEqual("1000 emails", charge.description)
+
+ def test_get_usage_charge(self):
+ self.fake(
+ "recurring_application_charges/654381177/usage_charges/359376002",
+ method="GET",
+ body=self.load_fixture("usage_charge"),
+ )
+
+ charge = shopify.UsageCharge.find(359376002, recurring_application_charge_id=654381177)
+ self.assertEqual("1000 emails", charge.description)
diff --git a/test/user_test.py b/test/user_test.py
new file mode 100644
index 00000000..efb90d30
--- /dev/null
+++ b/test/user_test.py
@@ -0,0 +1,26 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class UserTest(TestCase):
+ def test_get_all_users(self):
+ self.fake("users", body=self.load_fixture("users"))
+ users = shopify.User.find()
+
+ self.assertEqual(2, len(users))
+ self.assertEqual("Steve", users[0].first_name)
+ self.assertEqual("Jobs", users[0].last_name)
+
+ def test_get_user(self):
+ self.fake("users/799407056", body=self.load_fixture("user"))
+ user = shopify.User.find(799407056)
+
+ self.assertEqual("Steve", user.first_name)
+ self.assertEqual("Jobs", user.last_name)
+
+ def test_get_current_user(self):
+ self.fake("users/current", body=self.load_fixture("user"))
+ user = shopify.User.current()
+
+ self.assertEqual("Steve", user.first_name)
+ self.assertEqual("Jobs", user.last_name)
diff --git a/test/utils/shop_url_test.py b/test/utils/shop_url_test.py
new file mode 100644
index 00000000..ea7a77e9
--- /dev/null
+++ b/test/utils/shop_url_test.py
@@ -0,0 +1,47 @@
+from shopify.utils import shop_url
+from test.test_helper import TestCase
+
+
+class TestSanitizeShopDomain(TestCase):
+ def test_returns_hostname_for_good_shop_domains(self):
+ good_shop_domains = [
+ "my-shop",
+ "my-shop.myshopify.com",
+ "http://my-shop.myshopify.com",
+ "https://my-shop.myshopify.com",
+ ]
+ sanitized_shops = [shop_url.sanitize_shop_domain(shop_domain) for shop_domain in good_shop_domains]
+
+ self.assertTrue(all(shop == "my-shop.myshopify.com" for shop in sanitized_shops))
+
+ def test_returns_none_for_bad_shop_domains(self):
+ bad_shop_domains = [
+ "myshop.com",
+ "myshopify.com",
+ "shopify.com",
+ "two words",
+ "store.myshopify.com.evil.com",
+ "/foo/bar",
+ "/foo.myshopify.io.evil.ru",
+ "%0a123.myshopify.io ",
+ "foo.bar.myshopify.io",
+ ]
+ sanitized_shops = [shop_url.sanitize_shop_domain(shop_domain) for shop_domain in bad_shop_domains]
+
+ self.assertTrue(all(shop_domain is None for shop_domain in sanitized_shops))
+
+ def test_returns_hostname_for_custom_shop_domains(self):
+ custom_shop_domains = [
+ "my-shop",
+ "my-shop.myshopify.io",
+ "http://my-shop.myshopify.io",
+ "https://my-shop.myshopify.io",
+ ]
+ sanitized_shops = [
+ shop_url.sanitize_shop_domain(shop_domain, "myshopify.io") for shop_domain in custom_shop_domains
+ ]
+
+ self.assertTrue(all(shop == "my-shop.myshopify.io" for shop in sanitized_shops))
+
+ def test_returns_none_for_none_type(self):
+ self.assertIsNone(shop_url.sanitize_shop_domain(None))
diff --git a/test/variant_test.py b/test/variant_test.py
new file mode 100644
index 00000000..63ecb639
--- /dev/null
+++ b/test/variant_test.py
@@ -0,0 +1,49 @@
+import shopify
+from test.test_helper import TestCase
+
+
+class VariantTest(TestCase):
+ def test_get_variants(self):
+ self.fake("products/632910392/variants", method="GET", body=self.load_fixture("variants"))
+ v = shopify.Variant.find(product_id=632910392)
+
+ def test_get_variant_namespaced(self):
+ self.fake("products/632910392/variants/808950810", method="GET", body=self.load_fixture("variant"))
+ v = shopify.Variant.find(808950810, product_id=632910392)
+
+ def test_update_variant_namespace(self):
+ self.fake("products/632910392/variants/808950810", method="GET", body=self.load_fixture("variant"))
+ v = shopify.Variant.find(808950810, product_id=632910392)
+
+ self.fake(
+ "products/632910392/variants/808950810",
+ method="PUT",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
+ v.save()
+
+ def test_create_variant(self):
+ self.fake(
+ "products/632910392/variants",
+ method="POST",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
+ v = shopify.Variant({"product_id": 632910392})
+ v.save()
+
+ def test_create_variant_then_add_parent_id(self):
+ self.fake(
+ "products/632910392/variants",
+ method="POST",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
+ v = shopify.Variant()
+ v.product_id = 632910392
+ v.save()
+
+ def test_get_variant(self):
+ self.fake("variants/808950810", method="GET", body=self.load_fixture("variant"))
+ v = shopify.Variant.find(808950810)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..1523475c
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,23 @@
+[tox]
+envlist = py27, py34, py35, py36, py38, py39
+skip_missing_interpreters = true
+
+[testenv]
+setenv =
+ PYTHONPATH = {toxinidir}:{toxinidir}/shopify
+commands=
+ python setup.py test
+
+[testenv:flake8]
+basepython=python
+deps=
+ flake8
+ flake8_docstrings
+commands=
+ flake8 shopify
+
+[flake8]
+ignore = E126,E128
+max-line-length = 99
+exclude = .ropeproject
+max-complexity = 10