.*)\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
index d514eace..449e288b 100644
--- a/shopify/base.py
+++ b/shopify/base.py
@@ -4,36 +4,38 @@
import shopify.mixins as mixins
import shopify
import threading
-import urllib
-import urllib2
-import urlparse
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 __init__(self, site, user=None, password=None, timeout=None,
- format=formats.JSONFormat):
- super(ShopifyConnection, self).__init__(site, user, password, timeout, format)
-
def _open(self, *args, **kwargs):
self.response = None
try:
self.response = super(ShopifyConnection, self)._open(*args, **kwargs)
- except pyactiveresource.connection.ConnectionError, err:
+ 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):
+
+class ShopifyResourceMeta(ResourceMeta):
@property
def connection(cls):
"""HTTP connection for the current thread"""
local = cls._threadlocal
- if not getattr(local, 'connection', None):
+ 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
@@ -41,88 +43,122 @@ def connection(cls):
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)
+ 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)
+ 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.")
+ user = property(get_user, set_user, None, "The username for HTTP Basic Auth.")
def get_password(cls):
- return getattr(cls._threadlocal, 'password', ShopifyResource._password)
+ 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.")
+ password = property(get_password, set_password, None, "The password for HTTP Basic Auth.")
def get_site(cls):
- return getattr(cls._threadlocal, 'site', ShopifyResource._site)
+ 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:
- host = urlparse.urlsplit(value)[1]
- auth_info, host = urllib2.splituser(host)
- if auth_info:
- user, password = urllib2.splitpasswd(auth_info)
- if user:
- cls.user = urllib.unquote(user)
- if password:
- cls.password = urllib.unquote(password)
-
- site = property(get_site, set_site, None,
- 'The base REST site to connect to.')
+ 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)
+ 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')
+ timeout = property(get_timeout, set_timeout, None, "Socket timeout for HTTP requests")
def get_headers(cls):
- if not hasattr(cls._threadlocal, 'headers'):
+ 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')
+ headers = property(get_headers, set_headers, None, "The headers sent with HTTP requests")
def get_format(cls):
- return getattr(cls._threadlocal, 'format', ShopifyResource._format)
+ 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')
+ 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):
- __metaclass__ = ShopifyResourceMeta
_format = formats.JSONFormat
_threadlocal = threading.local()
- _headers = {'User-Agent': 'ShopifyPythonAPI/%s Python/%s' % (shopify.VERSION, sys.version.split(' ', 1)[0])}
+ _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:
@@ -139,13 +175,25 @@ def _load_attributes_from_response(self, response):
@classmethod
def activate_session(cls, session):
cls.site = session.site
+ cls.url = session.url
cls.user = None
cls.password = None
- cls.headers['X-Shopify-Access-Token'] = session.token
+ 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.headers.pop('X-Shopify-Access-Token', 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
index 9d3c1796..5a13ca3a 100644
--- a/shopify/mixins.py
+++ b/shopify/mixins.py
@@ -1,7 +1,7 @@
import shopify.resources
-class Countable(object):
+class Countable(object):
@classmethod
def count(cls, _options=None, **kwargs):
if _options is None:
@@ -10,20 +10,25 @@ def count(cls, _options=None, **kwargs):
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(self):
- return shopify.resources.Metafield.find(resource=self.__class__.plural, resource_id=self.id)
+ 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 = dict(resource=self.__class__.plural, resource_id=self.id)
+ 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
index 3abffba5..0d420b38 100644
--- a/shopify/resources/__init__.py
+++ b/shopify/resources/__init__.py
@@ -1,45 +1,82 @@
-from shop import Shop
-from product import Product
-from cart import Cart
-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 product_search_engine import ProductSearchEngine
-from application_charge import ApplicationCharge
-from recurring_application_charge import RecurringApplicationCharge
-from asset import Asset
-from theme import Theme
-from customer_saved_search import CustomerSavedSearch
-from customer_group import CustomerGroup
-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 fulfillment import Fulfillment
-from fulfillment_service import FulfillmentService
-from transaction import Transaction
-from image import Image
-from variant import Variant
-from order import Order
-from order_risk import OrderRisk
-from smart_collection import SmartCollection
+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/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
index df19e18c..6ed62f77 100644
--- a/shopify/resources/application_charge.py
+++ b/shopify/resources/application_charge.py
@@ -2,6 +2,5 @@
class ApplicationCharge(ShopifyResource):
-
def activate(self):
self._load_attributes_from_response(self.post("activate"))
diff --git a/shopify/resources/product_search_engine.py b/shopify/resources/application_credit.py
similarity index 51%
rename from shopify/resources/product_search_engine.py
rename to shopify/resources/application_credit.py
index 44976245..ecc12fa0 100644
--- a/shopify/resources/product_search_engine.py
+++ b/shopify/resources/application_credit.py
@@ -1,5 +1,5 @@
from ..base import ShopifyResource
-class ProductSearchEngine(ShopifyResource):
+class ApplicationCredit(ShopifyResource):
pass
diff --git a/shopify/resources/article.py b/shopify/resources/article.py
index cec30d5a..2b061a3e 100644
--- a/shopify/resources/article.py
+++ b/shopify/resources/article.py
@@ -1,26 +1,26 @@
from ..base import ShopifyResource
from shopify import mixins
-from comment import Comment
+from .comment import Comment
class Article(ShopifyResource, mixins.Metafields, mixins.Events):
- _prefix_source = "/admin/blogs/$blog_id/"
+ _prefix_source = "/blogs/$blog_id/"
@classmethod
def _prefix(cls, options={}):
blog_id = options.get("blog_id")
if blog_id:
- return "/admin/blogs/%s" % (blog_id)
+ return "%s/blogs/%s" % (cls.site, blog_id)
else:
- return "/admin"
+ return cls.site
def comments(self):
return Comment.find(article_id=self.id)
@classmethod
def authors(cls, **kwargs):
- return cls.get('authors', **kwargs)
+ return cls.get("authors", **kwargs)
@classmethod
def tags(cls, **kwargs):
- return cls.get('tags', **kwargs)
+ return cls.get("tags", **kwargs)
diff --git a/shopify/resources/asset.py b/shopify/resources/asset.py
index 8f8de2d8..d5156a5a 100644
--- a/shopify/resources/asset.py
+++ b/shopify/resources/asset.py
@@ -4,22 +4,26 @@
class Asset(ShopifyResource):
_primary_key = "key"
- _prefix_source = "/admin/themes/$theme_id/"
+ _prefix_source = "/themes/$theme_id/"
@classmethod
def _prefix(cls, options={}):
theme_id = options.get("theme_id")
if theme_id:
- return "/admin/themes/%s" % theme_id
+ return "%s/themes/%s" % (cls.site, theme_id)
else:
- return "/admin"
+ 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))
+ 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):
@@ -34,8 +38,10 @@ def find(cls, key=None, **kwargs):
params = {"asset[key]": key}
params.update(kwargs)
theme_id = params.get("theme_id")
- path_prefix = "/admin/themes/%s" % (theme_id) if theme_id else "/admin"
+ 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
@@ -46,7 +52,7 @@ def __get_value(self):
return data
data = self.attributes.get("attachment")
if data:
- return base64.b64decode(data)
+ return base64.b64decode(data).decode()
def __set_value(self, data):
self.__wipe_value_attributes()
@@ -55,7 +61,7 @@ def __set_value(self, data):
value = property(__get_value, __set_value, None, "The asset's value or attachment")
def attach(self, data):
- self.attachment = base64.b64encode(data)
+ self.attachment = base64.b64encode(data).decode()
def destroy(self):
options = {"asset[key]": self.key}
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/blog.py b/shopify/resources/blog.py
index c094560e..e88b26b1 100644
--- a/shopify/resources/blog.py
+++ b/shopify/resources/blog.py
@@ -1,9 +1,8 @@
from ..base import ShopifyResource
from shopify import mixins
-from article import Article
+import shopify
class Blog(ShopifyResource, mixins.Metafields, mixins.Events):
-
def articles(self):
- return Article.find(blog_id=self.id)
+ 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/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/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
index 0015eb74..110afd61 100644
--- a/shopify/resources/comment.py
+++ b/shopify/resources/comment.py
@@ -2,7 +2,6 @@
class Comment(ShopifyResource):
-
def remove(self):
self._load_attributes_from_response(self.post("remove"))
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
index fe648bee..85bcbc4a 100644
--- a/shopify/resources/custom_collection.py
+++ b/shopify/resources/custom_collection.py
@@ -1,18 +1,16 @@
from ..base import ShopifyResource
from shopify import mixins
-from collect import Collect
-import product
+import shopify
class CustomCollection(ShopifyResource, mixins.Metafields, mixins.Events):
-
def products(self):
- return product.Product.find(collection_id=self.id)
+ return shopify.Product.find(collection_id=self.id)
def add_product(self, product):
- return Collect.create({'collection_id': self.id, 'product_id': product.id})
+ return shopify.Collect.create({"collection_id": self.id, "product_id": product.id})
def remove_product(self, product):
- collect = Collect.find_first(collection_id=self.id, product_id=product.id)
+ 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
index 2855962a..ab989e84 100644
--- a/shopify/resources/customer.py
+++ b/shopify/resources/customer.py
@@ -1,20 +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:
- q: Text to search for customers ("q" is short for query)
- f: Filters to apply to customers ("f" is short for query)
+ 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: Maximum number of results to show (default: 50, maximum: 250)
+ limit: Amount of results (default: 50) (maximum: 250)
+ fields: comma-separated list of fields to include in the response
Returns:
- An array of customers.
+ A Collection of customers.
"""
- return cls._build_list(cls.get("search", **kwargs))
+ 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
index 900c0b4a..0eb48de2 100644
--- a/shopify/resources/customer_group.py
+++ b/shopify/resources/customer_group.py
@@ -1,4 +1,4 @@
-from customer_saved_search import CustomerSavedSearch
+from .customer_saved_search import CustomerSavedSearch
class CustomerGroup(CustomerSavedSearch):
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
index a19dae48..dbe97251 100644
--- a/shopify/resources/customer_saved_search.py
+++ b/shopify/resources/customer_saved_search.py
@@ -1,8 +1,7 @@
from ..base import ShopifyResource
-from customer import Customer
+from .customer import Customer
class CustomerSavedSearch(ShopifyResource):
-
def customers(cls, **kwargs):
- return Customer._build_list(cls.get("customers", **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
index 8b25ce38..f3268e13 100644
--- a/shopify/resources/event.py
+++ b/shopify/resources/event.py
@@ -1,12 +1,13 @@
from ..base import ShopifyResource
+
class Event(ShopifyResource):
- _prefix_source = "/admin/$resource/$resource_id/"
+ _prefix_source = "/$resource/$resource_id/"
@classmethod
def _prefix(cls, options={}):
resource = options.get("resource")
if resource:
- return "/admin/%s/%s" % (resource, options["resource_id"])
+ return "%s/%s/%s" % (cls.site, resource, options["resource_id"])
else:
- return "/admin"
+ return cls.site
diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py
index 10096a8e..fcf74863 100644
--- a/shopify/resources/fulfillment.py
+++ b/shopify/resources/fulfillment.py
@@ -1,11 +1,33 @@
from ..base import ShopifyResource
+import json
class Fulfillment(ShopifyResource):
- _prefix_source = "/admin/orders/$order_id/"
+ _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
index f303fe38..c2c465b7 100644
--- a/shopify/resources/fulfillment_service.py
+++ b/shopify/resources/fulfillment_service.py
@@ -2,4 +2,10 @@
class FulfillmentService(ShopifyResource):
- pass
+ 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
index 755c09e7..1a4d13fb 100644
--- a/shopify/resources/image.py
+++ b/shopify/resources/image.py
@@ -1,10 +1,20 @@
from ..base import ShopifyResource
+from ..resources import Metafield
+from six.moves import urllib
import base64
import re
class Image(ShopifyResource):
- _prefix_source = "/admin/products/$product_id/"
+ _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"]:
@@ -13,6 +23,19 @@ def __getattr__(self, name):
return super(Image, self).__getattr__(name)
def attach_image(self, data, filename=None):
- self.attributes["attachment"] = base64.b64encode(data)
+ 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
index ced3a46b..c701c90f 100644
--- a/shopify/resources/line_item.py
+++ b/shopify/resources/line_item.py
@@ -2,4 +2,5 @@
class LineItem(ShopifyResource):
- pass
+ 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
index f37af161..7cba8e8e 100644
--- a/shopify/resources/metafield.py
+++ b/shopify/resources/metafield.py
@@ -2,12 +2,12 @@
class Metafield(ShopifyResource):
- _prefix_source = "/admin/$resource/$resource_id/"
+ _prefix_source = "/$resource/$resource_id/"
@classmethod
def _prefix(cls, options={}):
resource = options.get("resource")
if resource:
- return "/admin/%s/%s" % (resource, options["resource_id"])
+ return "%s/%s/%s" % (cls.site, resource, options["resource_id"])
else:
- return "/admin"
+ return cls.site
diff --git a/shopify/resources/order.py b/shopify/resources/order.py
index d8adb6ad..2e31a8c3 100644
--- a/shopify/resources/order.py
+++ b/shopify/resources/order.py
@@ -1,9 +1,18 @@
from ..base import ShopifyResource
from shopify import mixins
-from transaction import Transaction
+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"))
@@ -18,4 +27,4 @@ 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)
+ 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
index 2cc4c268..fdcfa1f3 100644
--- a/shopify/resources/order_risk.py
+++ b/shopify/resources/order_risk.py
@@ -1,5 +1,7 @@
from ..base import ShopifyResource
+
class OrderRisk(ShopifyResource):
- _prefix_source = "/admin/orders/$order_id/"
- _plural = "risks"
+ _prefix_source = "/orders/$order_id/"
+ _singular = "risk"
+ _plural = "risks"
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
index 34691168..cc16e3e3 100644
--- a/shopify/resources/product.py
+++ b/shopify/resources/product.py
@@ -1,13 +1,11 @@
from ..base import ShopifyResource
from shopify import mixins
-from custom_collection import CustomCollection
-from smart_collection import SmartCollection
+import shopify
class Product(ShopifyResource, mixins.Metafields, mixins.Events):
-
def price_range(self):
- prices = [variant.price for variant in self.variants]
+ prices = [float(variant.price) for variant in self.variants]
f = "%0.2f"
min_price = min(prices)
max_price = max(prices)
@@ -17,13 +15,29 @@ def price_range(self):
return f % min_price
def collections(self):
- return CustomCollection.find(product_id=self.id)
+ return shopify.CustomCollection.find(product_id=self.id)
def smart_collections(self):
- return SmartCollection.find(product_id=self.id)
+ 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/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/recurring_application_charge.py b/shopify/resources/recurring_application_charge.py
index f0e30045..c94cac2d 100644
--- a/shopify/resources/recurring_application_charge.py
+++ b/shopify/resources/recurring_application_charge.py
@@ -1,14 +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):
- return cls.find_first(status="active")
-
- def cancel(self):
- self._load_attributes_from_response(self.destroy)
+ """
+ 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/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/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
index e179eef4..4d447366 100644
--- a/shopify/resources/shop.py
+++ b/shopify/resources/shop.py
@@ -1,13 +1,12 @@
from ..base import ShopifyResource
-from metafield import Metafield
-from event import Event
+from .metafield import Metafield
+from .event import Event
class Shop(ShopifyResource):
-
@classmethod
def current(cls):
- return cls.find_one("/admin/shop." + cls.format.extension)
+ return cls.find_one(cls.site + "/shop." + cls.format.extension)
def metafields(self):
return Metafield.find()
diff --git a/shopify/resources/smart_collection.py b/shopify/resources/smart_collection.py
index 1ff4e607..802c3a7e 100644
--- a/shopify/resources/smart_collection.py
+++ b/shopify/resources/smart_collection.py
@@ -1,9 +1,8 @@
from ..base import ShopifyResource
from shopify import mixins
-import product
+import shopify
class SmartCollection(ShopifyResource, mixins.Metafields, mixins.Events):
-
def products(self):
- return product.Product.find(collection_id=self.id)
+ 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/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/transaction.py b/shopify/resources/transaction.py
index aaed92b9..f465255a 100644
--- a/shopify/resources/transaction.py
+++ b/shopify/resources/transaction.py
@@ -2,4 +2,4 @@
class Transaction(ShopifyResource):
- _prefix_source = "/admin/orders/$order_id/"
+ _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
index 3263127b..743b071b 100644
--- a/shopify/resources/variant.py
+++ b/shopify/resources/variant.py
@@ -3,12 +3,26 @@
class Variant(ShopifyResource, mixins.Metafields):
- _prefix_source = "/admin/products/$product_id/"
+ _prefix_source = "/products/$product_id/"
@classmethod
def _prefix(cls, options={}):
product_id = options.get("product_id")
if product_id:
- return "/admin/products/%s" % (product_id)
+ return "%s/products/%s" % (cls.site, product_id)
else:
- return "/admin"
+ 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
index 452934f3..ba8a7f28 100644
--- a/shopify/resources/webhook.py
+++ b/shopify/resources/webhook.py
@@ -2,7 +2,6 @@
class Webhook(ShopifyResource):
-
def __get_format(self):
return self.attributes.get("format")
diff --git a/shopify/session.py b/shopify/session.py
index 74972ea4..561faacf 100644
--- a/shopify/session.py
+++ b/shopify/session.py
@@ -1,108 +1,185 @@
import time
-import urllib
-import urllib2
-try:
- from hashlib import md5
-except ImportError:
- from md5 import md5
+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'
+ protocol = "https"
+ myshopify_domain = "myshopify.com"
+ port = None
@classmethod
def setup(cls, **kwargs):
- for k, v in kwargs.iteritems():
+ for k, v in six.iteritems(kwargs):
setattr(cls, k, v)
@classmethod
@contextmanager
- def temp(cls, domain, token):
+ def temp(cls, domain, version, token):
import shopify
- original_domain = shopify.ShopifyResource.get_site()
- original_token = shopify.ShopifyResource.headers['X-Shopify-Access-Token']
- original_session = shopify.Session(original_domain, original_token)
- session = Session(domain, token)
+ 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, token=None, params=None):
+ 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, scope, redirect_uri=None):
- query_params = dict(client_id=self.api_key, scope=",".join(scope))
- if redirect_uri: query_params['redirect_uri'] = redirect_uri
- return "%s://%s/admin/oauth/authorize?%s" % (self.protocol, self.url, urllib.urlencode(query_params))
+ 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 Exception('Invalid Signature: Possibly malicious login')
+ raise ValidationException("Invalid HMAC: Possibly malicious login")
- code = params['code']
+ code = params["code"]
- url = "%s://%s/admin/oauth/access_token?" % (self.protocol, self.url)
- query_params = dict(client_id=self.api_key, client_secret=self.secret, code=code)
- request = urllib2.Request(url, urllib.urlencode(query_params))
- response = urllib2.urlopen(request)
+ 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:
- self.token = json.loads(response.read())['access_token']
+ 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 "%s://%s/admin" % (self.protocol, self.url)
+ 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
- @staticmethod
- def __prepare_url(url):
- if url.strip() == "":
+ @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)
- url = re.sub("/.*", "", url)
- if url.find(".") == -1:
- url += ".myshopify.com"
- return url
+ 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['timestamp']) < time.time() - one_day:
+ if int(params.get("timestamp", 0)) < time.time() - one_day:
return False
- return cls.validate_signature(params)
+ return cls.validate_hmac(params)
@classmethod
- def validate_signature(cls, params):
- if "signature" not in params:
+ def validate_hmac(cls, params):
+ if "hmac" not in params:
return False
- sorted_params = ""
- signature = params['signature']
+ hmac_calculated = cls.calculate_hmac(params).encode("utf-8")
+ hmac_to_verify = params["hmac"].encode("utf-8")
- for k in sorted(params.keys()):
- if k != "signature":
- sorted_params += k + "=" + str(params[k])
+ # 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
- return md5(cls.secret + sorted_params).hexdigest() == signature
+ @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
index 2f7444bb..dfb0b4e4 100644
--- a/shopify/version.py
+++ b/shopify/version.py
@@ -1 +1 @@
-VERSION = '2.0.3'
+VERSION = "12.7.1"
diff --git a/shopify/yamlobjects.py b/shopify/yamlobjects.py
index d4e7cd3a..c7438c42 100644
--- a/shopify/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/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
index d8ec7022..5ee53bbe 100644
--- a/test/article_test.py
+++ b/test/article_test.py
@@ -1,58 +1,75 @@
import shopify
-from test_helper import TestCase
+from test.test_helper import TestCase
-class ArticleTest(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})
+ 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'))
+ 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'))
+ 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'))
+ 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'))
+ 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'))
+ 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'))
+ 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'))
+ 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'))
+ 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'))
+ 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
index dd12d9d4..19606a4b 100644
--- a/test/asset_test.py
+++ b/test/asset_test.py
@@ -1,16 +1,90 @@
+import base64
+
import shopify
-from test_helper import TestCase
+from test.test_helper import TestCase
-class AssetTest(TestCase):
+class AssetTest(TestCase):
def test_get_assets(self):
- self.fake("themes/1/assets", method='GET', body=self.load_fixture('assets'))
- v = shopify.Asset.find(theme_id = 1)
+ 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)
+ 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_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_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
index 9e022a94..5cc19a60 100644
--- a/test/base_test.py
+++ b/test/base_test.py
@@ -1,18 +1,21 @@
import shopify
-from test_helper import TestCase
+from test.test_helper import TestCase
from pyactiveresource.activeresource import ActiveResource
from mock import patch
import threading
-class BaseTest(TestCase):
+class BaseTest(TestCase):
@classmethod
def setUpClass(self):
- self.session1 = shopify.Session('shop1.myshopify.com', 'token1')
- self.session2 = shopify.Session('shop2.myshopify.com', 'token2')
+ 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")
- def setUp(self):
- super(BaseTest, self).setUp()
+ @classmethod
+ def tearDownClass(self):
+ shopify.ApiVersion.clear_defined_versions()
def tearDown(self):
shopify.ShopifyResource.clear_session()
@@ -21,13 +24,21 @@ 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', shopify.ShopifyResource.site)
- self.assertEqual('https://shop1.myshopify.com/admin', shopify.Shop.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'])
+ self.assertEqual("token1", shopify.ShopifyResource.headers["X-Shopify-Access-Token"])
+ self.assertEqual("token1", shopify.Shop.headers["X-Shopify-Access-Token"])
- def test_clear_session_should_clear_site_and_headers_from_Base(self):
+ 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()
@@ -36,49 +47,68 @@ def test_clear_session_should_clear_site_and_headers_from_Base(self):
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)
+ 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):
+ 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.clear_session()
shopify.ShopifyResource.activate_session(self.session2)
self.assertIsNone(ActiveResource.site)
- self.assertEqual('https://shop2.myshopify.com/admin', shopify.ShopifyResource.site)
- self.assertEqual('https://shop2.myshopify.com/admin', shopify.Shop.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'])
+ 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'})
+ org_headers = shopify.ShopifyResource.headers
+ shopify.ShopifyResource.set_headers({"X-Custom": "abc"})
- with patch('shopify.ShopifyResource.connection.delete') as mock:
- shopify.ShopifyResource.delete('1')
- mock.assert_called_with('/admin/shopify_resources/1.json', {'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))
+ 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)
+ 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 = 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
index 465df603..e3a912f0 100644
--- a/test/blog_test.py
+++ b/test/blog_test.py
@@ -1,9 +1,15 @@
import shopify
-from test_helper import TestCase
+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.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
index 424c5b6a..67836330 100644
--- a/test/cart_test.py
+++ b/test/cart_test.py
@@ -1,13 +1,13 @@
import shopify
-from test_helper import TestCase
+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)
+ 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
index 4f5f6e2f..48873f15 100644
--- a/test/customer_saved_search_test.py
+++ b/test/customer_saved_search_test.py
@@ -1,23 +1,29 @@
import shopify
-from test_helper import TestCase
+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.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.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.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
index 65ac8c6d..9910f0eb 100644
--- a/test/fixtures/article.json
+++ b/test/fixtures/article.json
@@ -12,4 +12,4 @@
"user_id": null,
"tags": "consequuntur, cupiditate, repellendus"
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/articles.json b/test/fixtures/articles.json
index df83a9cb..5954b09f 100644
--- a/test/fixtures/articles.json
+++ b/test/fixtures/articles.json
@@ -36,4 +36,4 @@
"user_id": 2221540,
"tags": ""
}]
-}
\ No newline at end of file
+}
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
index df94412c..2c92f6d7 100644
--- a/test/fixtures/blog.json
+++ b/test/fixtures/blog.json
@@ -10,4 +10,4 @@
"feedburner": null,
"commentable": "no"
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/blogs.json b/test/fixtures/blogs.json
index 3f779b25..0749df28 100644
--- a/test/fixtures/blogs.json
+++ b/test/fixtures/blogs.json
@@ -10,4 +10,4 @@
"feedburner": null,
"commentable": "no"
}]
-}
\ No newline at end of file
+}
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
index 64d51246..4238684f 100644
--- a/test/fixtures/carts.json
+++ b/test/fixtures/carts.json
@@ -5,7 +5,7 @@
"note": null,
"token": "3eed8183d4281db6ea82ee2b8f23e9cc",
"updated_at": "2012-02-13T14:39:37-05:00",
- "line_items":
+ "line_items":
[
{
"id": 1,
@@ -40,4 +40,4 @@
]
}
]
-}
\ No newline at end of file
+}
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/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/fulfillment.json b/test/fixtures/fulfillment.json
index 9d7e04d3..6d8bd9e8 100644
--- a/test/fixtures/fulfillment.json
+++ b/test/fixtures/fulfillment.json
@@ -5,7 +5,7 @@
"order_id": 450789469,
"service": "manual",
"status": "pending",
- "tracking_company": null,
+ "tracking_company": "null-company",
"updated_at": "2013-11-01T16:06:08-04:00",
"tracking_number": "1Z2345",
"tracking_numbers": [
@@ -46,4 +46,4 @@
}
]
}
-}
\ No newline at end of file
+}
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/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_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/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/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/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_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/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/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
index 33a70e9d..61ae6f98 100644
--- a/test/fixtures/transaction.json
+++ b/test/fixtures/transaction.json
@@ -26,4 +26,4 @@
"credit_card_company": "Visa"
}
}
-}
\ No newline at end of file
+}
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/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
index 12bb42bf..7519b3b9 100644
--- a/test/fulfillment_service_test.py
+++ b/test/fulfillment_service_test.py
@@ -1,15 +1,26 @@
import shopify
-from test_helper import TestCase
+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'})
+ self.fake(
+ "fulfillment_services",
+ method="POST",
+ body=self.load_fixture("fulfillment_service"),
+ headers={"Content-type": "application/json"},
+ )
- fulfillment_service = shopify.FulfillmentService.create({'name': "SomeService"})
+ 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'))
+ 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
index 91c6bed6..8bb84efb 100644
--- a/test/fulfillment_test.py
+++ b/test/fulfillment_test.py
@@ -1,31 +1,83 @@
import shopify
-from test_helper import TestCase
+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'))
+ 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('pending','success')
- self.fake("orders/450789469/fulfillments/255858046/complete", method='POST', headers={'Content-length':'0', 'Content-type': 'application/json'}, body=success)
+ 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)
+ self.assertEqual("pending", fulfillment.status)
fulfillment.complete()
- self.assertEqual('success', fulfillment.status)
-
+ 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('pending', 'cancelled')
- self.fake("orders/450789469/fulfillments/255858046/cancel", method='POST', headers={'Content-length':'0', 'Content-type': 'application/json'}, body=cancelled)
+ 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)
+ self.assertEqual("pending", fulfillment.status)
fulfillment.cancel()
- self.assertEqual('cancelled', fulfillment.status)
+ 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
index 92aabfa0..3dad3817 100644
--- a/test/image_test.py
+++ b/test/image_test.py
@@ -1,24 +1,84 @@
import shopify
-from test_helper import TestCase
+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()
- 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_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)
+ 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
index ad790105..4ca6dbff 100644
--- a/test/order_risk_test.py
+++ b/test/order_risk_test.py
@@ -1,42 +1,52 @@
import shopify
-from test_helper import TestCase
+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()
+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
index 8b7bca96..257ab08a 100644
--- a/test/order_test.py
+++ b/test/order_test.py
@@ -1,10 +1,10 @@
import shopify
-from test_helper import TestCase
+from test.test_helper import TestCase
from pyactiveresource.activeresource import ActiveResource
from pyactiveresource.util import xml_to_dict
-class OrderTest(TestCase):
+class OrderTest(TestCase):
def test_should_be_loaded_correctly_from_order_xml(self):
order_xml = """
@@ -26,7 +26,7 @@ def test_should_be_loaded_correctly_from_order_xml(self):
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.note_attributes.append(shopify.NoteAttribute({"name": "color", "value": "blue"}))
order_xml = xml_to_dict(order.to_xml())
note_attributes = order_xml["order"]["note_attributes"]
@@ -37,13 +37,20 @@ def test_should_be_able_to_add_note_attributes_to_an_order(self):
self.assertEqual("blue", attribute["value"])
def test_get_order(self):
- self.fake('orders/450789469', method='GET', body=self.load_fixture('order'))
+ self.fake("orders/450789469", method="GET", body=self.load_fixture("order"))
order = shopify.Order.find(450789469)
- self.assertEqual('bob.norman@hostmail.com', order.email)
+ 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'))
+ 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('transaction'))
+ 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
index 89c17bfe..de183691 100644
--- a/test/product_test.py
+++ b/test/product_test.py
@@ -1,18 +1,28 @@
import shopify
-from test_helper import TestCase
+from test.test_helper import TestCase
-class ProductTest(TestCase):
+class ProductTest(TestCase):
def setUp(self):
super(ProductTest, self).setUp()
- self.fake("products/632910392", body=self.load_fixture('product'))
+ 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'})
+ 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"}))
+ 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)
@@ -20,7 +30,7 @@ def test_add_metafields_to_product(self):
self.assertEqual("123@example.com", field.value)
def test_get_metafields_for_product(self):
- self.fake("products/632910392/metafields", body=self.load_fixture('metafields'))
+ self.fake("products/632910392/metafields", body=self.load_fixture("metafields"))
metafields = self.product.metafields()
@@ -28,9 +38,50 @@ def test_get_metafields_for_product(self):
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'))
+ 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
index d453c548..c785388e 100644
--- a/test/recurring_charge_test.py
+++ b/test/recurring_charge_test.py
@@ -1,9 +1,63 @@
import shopify
-from test_helper import TestCase
+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})
+ 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
index 459ee1c4..8d73e293 100644
--- a/test/session_test.py
+++ b/test/session_test.py
@@ -1,32 +1,46 @@
import shopify
-from test_helper import TestCase
-try:
- from hashlib import md5
-except ImportError:
- from md5 import md5
+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("", "any-token")
+ session = shopify.Session("", "unstable", "any-token")
self.assertFalse(session.valid)
def test_not_be_valid_without_token(self):
- session = shopify.Session("testshop.myshopify.com")
+ 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", "any-token")
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
self.assertTrue(session.valid)
- def test_not_raise_error_without_params(self):
- session = shopify.Session("testshop.myshopify.com", "any-token")
+ 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(Exception):
- session = shopify.Session("testshop.myshopify.com")
- token = session.request_token('code', {'foo': 'bar'})
+ 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")
@@ -34,114 +48,274 @@ def test_setup_api_key_and_secret_for_all_sessions(self):
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)
+ self.assertEqual("https", shopify.Session.protocol)
- def test_temp_reset_shopify_ShopifyResource_site_to_original_value(self):
+ 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', 'token1')
+ session1 = shopify.Session("fakeshop.myshopify.com", "2019-04", "token1")
shopify.ShopifyResource.activate_session(session1)
assigned_site = ""
- with shopify.Session.temp("testshop.myshopify.com", "any-token"):
+ with shopify.Session.temp("testshop.myshopify.com", "unstable", "any-token"):
assigned_site = shopify.ShopifyResource.site
- self.assertEqual('https://testshop.myshopify.com/admin', assigned_site)
- self.assertEqual('https://fakeshop.myshopify.com/admin', 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_temp_reset_shopify_ShopifyResource_site_to_original_value_when_using_a_non_standard_port(self):
- shopify.Session.setup(api_key="key", secret="secret")
- session1 = shopify.Session('fakeshop.myshopify.com:3000', 'token1')
- shopify.ShopifyResource.activate_session(session1)
+ 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", "any-token"):
+ with shopify.Session.temp("testshop.myshopify.com", "unstable", "any-token"):
assigned_site = shopify.ShopifyResource.site
- self.assertEqual('https://testshop.myshopify.com/admin', assigned_site)
- self.assertEqual('https://fakeshop.myshopify.com:3000/admin', 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_single_scope_no_redirect_uri(self):
+ 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')
- scope = ["write_products"]
- permission_url = session.create_permission_url(scope)
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?scope=write_products&client_id=My_test_key", permission_url)
+ 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_single_scope_and_redirect_uri(self):
+ 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')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
scope = ["write_products"]
- permission_url = session.create_permission_url(scope, "my_redirect_uri.com")
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?scope=write_products&redirect_uri=my_redirect_uri.com&client_id=My_test_key", permission_url)
+ 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_dual_scope_no_redirect_uri(self):
+ 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')
- scope = ["write_products","write_customers"]
- permission_url = session.create_permission_url(scope)
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?scope=write_products%2Cwrite_customers&client_id=My_test_key", permission_url)
+ 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_no_scope_no_redirect_uri(self):
+ 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')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
scope = []
- permission_url = session.create_permission_url(scope)
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?scope=&client_id=My_test_key", permission_url)
+ 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')
- self.fake(None, url='https://localhost.myshopify.com/admin/oauth/access_token', method='POST', code=404, body='{"error" : "invalid_request"}', has_user_agent=False)
+ 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(Exception):
- session.request_token({"code":"any-code"})
+ 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", "any-token")
- self.assertEqual("https://testshop.myshopify.com/admin", session.site)
-
- def test_return_token_if_signature_is_valid(self):
- shopify.Session.secret='secret'
- params = {'code': 'any-code', 'timestamp': time.time()}
- sorted_params = self.make_sorted_params(params)
- signature = md5(shopify.Session.secret + sorted_params).hexdigest()
- params['signature'] = signature
-
- self.fake(None, url='https://localhost.myshopify.com/admin/oauth/access_token', method='POST', body='{"access_token" : "token"}', has_user_agent=False)
- session = shopify.Session('http://localhost.myshopify.com')
+ 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_signature_does_not_match_expected(self):
- shopify.Session.secret='secret'
- params = {'foo': 'hello', 'timestamp': time.time()}
- sorted_params = self.make_sorted_params(params)
- signature = md5(shopify.Session.secret + sorted_params).hexdigest()
- params['signature'] = signature
- params['bar'] = 'world'
+ 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(Exception):
- session = shopify.Session('http://localhost.myshopify.com')
- session = session.request_token("code", params=params)
+ 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'
+ shopify.Session.secret = "secret"
one_day = 24 * 60 * 60
- params = {'code': 'any-code', 'timestamp': time.time()-(2*one_day)}
- sorted_params = self.make_sorted_params(params)
- signature = md5(shopify.Session.secret + sorted_params).hexdigest()
- params['signature'] = signature
+ params = {"code": "any-code", "timestamp": time.time() - (2 * one_day)}
+ hmac = shopify.Session.calculate_hmac(params)
+ params["hmac"] = hmac
- with self.assertRaises(Exception):
- session = shopify.Session('http://localhost.myshopify.com')
+ 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 make_sorted_params(self, params):
- sorted_params = ""
- for k in sorted(params.keys()):
- if k != "signature":
- sorted_params += k + "=" + str(params[k])
- return sorted_params
+ 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
index 66be7d4a..3a88a2a6 100644
--- a/test/shop_test.py
+++ b/test/shop_test.py
@@ -1,5 +1,6 @@
import shopify
-from test_helper import TestCase
+from test.test_helper import TestCase
+
class ShopTest(TestCase):
def setUp(self):
@@ -8,7 +9,7 @@ def setUp(self):
self.shop = shopify.Shop.current()
def test_current_should_return_current_shop(self):
- self.assertTrue(isinstance(self.shop,shopify.Shop))
+ 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)
@@ -25,9 +26,19 @@ def test_get_metafields_for_shop(self):
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.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)
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
index efb27a22..666ac792 100644
--- a/test/test_helper.py
+++ b/test/test_helper.py
@@ -5,52 +5,55 @@
from pyactiveresource.testing import http_fake
import shopify
-class TestCase(unittest.TestCase):
+class TestCase(unittest.TestCase):
def setUp(self):
ActiveResource.site = None
- ActiveResource.headers=None
+ ActiveResource.headers = None
shopify.ShopifyResource.clear_session()
- shopify.ShopifyResource.site = "http://localhost/admin"
+ 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://localhost'
+ 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'):
- return open(os.path.dirname(__file__)+'/fixtures/%s.%s' % (name, format), 'r').read()
+ 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')
-
- if ('extension' in kwargs and not kwargs['extension']):
+ 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'))
+ extension = ".%s" % (kwargs.pop("extension", "json"))
- url = "http://localhost/admin/%s%s" % (endpoint, extension)
+ url = "https://this-is-my-test-show.myshopify.com%s/%s%s" % (prefix, endpoint, extension)
try:
- url = kwargs['url']
+ url = kwargs["url"]
except KeyError:
- pass
+ 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
+ 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'])
+ headers.update(kwargs["headers"])
except KeyError:
- pass
+ pass
- code = kwargs.pop('code', 200)
+ code = kwargs.pop("code", 200)
self.http.respond_to(
- method, url, headers, body=body, code=code)
+ 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
index ae32677f..e02fae00 100644
--- a/test/transaction_test.py
+++ b/test/transaction_test.py
@@ -1,10 +1,11 @@
import shopify
-from test_helper import TestCase
+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'))
+ 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)
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
index d4cfd774..63ecb639 100644
--- a/test/variant_test.py
+++ b/test/variant_test.py
@@ -1,16 +1,49 @@
import shopify
-from test_helper import TestCase
+from test.test_helper import TestCase
-class VariantTest(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)
+ 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)
+ 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'))
+ 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