.*)\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 d3539f7e..449e288b 100644
--- a/shopify/base.py
+++ b/shopify/base.py
@@ -8,15 +8,15 @@
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:
@@ -26,14 +26,16 @@ def _open(self, *args, **kwargs):
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,85 +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:
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.')
+ 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):
_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:
@@ -136,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 c7806a0c..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,7 +10,6 @@ def count(cls, _options=None, **kwargs):
class Metafields(object):
-
def metafields(self, _options=None, **kwargs):
if _options is None:
_options = kwargs
@@ -25,12 +24,11 @@ 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 d2ee5e02..0d420b38 100644
--- a/shopify/resources/__init__.py
+++ b/shopify/resources/__init__.py
@@ -16,14 +16,16 @@
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 .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
@@ -36,21 +38,45 @@
from .page import Page
from .country import Country
from .refund import Refund
-from .fulfillment import Fulfillment
+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 .discount import Discount
+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 dccbe0b3..2b061a3e 100644
--- a/shopify/resources/article.py
+++ b/shopify/resources/article.py
@@ -4,23 +4,23 @@
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 19a7bd76..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,7 +38,7 @@ 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)
@@ -48,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()
@@ -57,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 d623f011..e88b26b1 100644
--- a/shopify/resources/blog.py
+++ b/shopify/resources/blog.py
@@ -4,6 +4,5 @@
class Blog(ShopifyResource, mixins.Metafields, mixins.Events):
-
def articles(self):
return shopify.Article.find(blog_id=self.id)
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 87ffae1a..85bcbc4a 100644
--- a/shopify/resources/custom_collection.py
+++ b/shopify/resources/custom_collection.py
@@ -4,12 +4,11 @@
class CustomCollection(ShopifyResource, mixins.Metafields, mixins.Events):
-
def products(self):
return shopify.Product.find(collection_id=self.id)
def add_product(self, product):
- return shopify.Collect.create({'collection_id': self.id, 'product_id': product.id})
+ return shopify.Collect.create({"collection_id": self.id, "product_id": product.id})
def remove_product(self, product):
collect = shopify.Collect.find_first(collection_id=self.id, product_id=product.id)
diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py
index cb33e5c5..ab989e84 100644
--- a/shopify/resources/customer.py
+++ b/shopify/resources/customer.py
@@ -1,9 +1,10 @@
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):
"""
@@ -14,8 +15,15 @@ def search(cls, **kwargs):
query: Text to search for customers
page: Page to show (default: 1)
limit: Amount of results (default: 50) (maximum: 250)
- fields: comma-seperated list of fields to include in the response
+ 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_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 88c74acc..dbe97251 100644
--- a/shopify/resources/customer_saved_search.py
+++ b/shopify/resources/customer_saved_search.py
@@ -3,6 +3,5 @@
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.py b/shopify/resources/discount.py
deleted file mode 100644
index 1755ef86..00000000
--- a/shopify/resources/discount.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from ..base import ShopifyResource
-
-
-class Discount(ShopifyResource):
-
- def disable(self):
- self._load_attributes_from_response(self.post("disable"))
-
- def enable(self):
- self._load_attributes_from_response(self.post("enable"))
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
index b8492d36..878cb7af 100644
--- a/shopify/resources/draft_order.py
+++ b/shopify/resources/draft_order.py
@@ -4,12 +4,12 @@
class DraftOrder(ShopifyResource, mixins.Metafields):
- def send_invoice(self, draft_order_invoice = DraftOrderInvoice()):
+ 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'))
+ 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"))
+ self._load_attributes_from_response(self.put("complete"))
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 c15e3d48..fcf74863 100644
--- a/shopify/resources/fulfillment.py
+++ b/shopify/resources/fulfillment.py
@@ -1,8 +1,9 @@
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"))
@@ -12,3 +13,21 @@ def complete(self):
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/gift_card.py b/shopify/resources/gift_card.py
index 9476478e..c1918c68 100644
--- a/shopify/resources/gift_card.py
+++ b/shopify/resources/gift_card.py
@@ -1,7 +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 a0e82efe..1a4d13fb 100644
--- a/shopify/resources/image.py
+++ b/shopify/resources/image.py
@@ -6,15 +6,15 @@
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 "/admin/products/%s" % (product_id)
+ return "%s/products/%s" % (cls.site, product_id)
else:
- return "/admin"
+ return cls.site
def __getattr__(self, name):
if name in ["pico", "icon", "thumb", "small", "compact", "medium", "large", "grande", "original"]:
@@ -23,17 +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_ = '/admin/metafields.json?%s' % urllib.parse.urlencode(query_params))
+ 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
+ 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 cac2ceed..c701c90f 100644
--- a/shopify/resources/line_item.py
+++ b/shopify/resources/line_item.py
@@ -2,5 +2,5 @@
class LineItem(ShopifyResource):
- class Property(ShopifyResource):
- pass
+ class Property(ShopifyResource):
+ pass
diff --git a/shopify/resources/location.py b/shopify/resources/location.py
index 671b5b0e..51e7ecdd 100644
--- a/shopify/resources/location.py
+++ b/shopify/resources/location.py
@@ -1,5 +1,9 @@
from ..base import ShopifyResource
+from .inventory_level import InventoryLevel
class Location(ShopifyResource):
- pass
+ 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 4e780b0a..2e31a8c3 100644
--- a/shopify/resources/order.py
+++ b/shopify/resources/order.py
@@ -4,6 +4,15 @@
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"))
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
index caac98b9..d97fcc2e 100644
--- a/shopify/resources/policy.py
+++ b/shopify/resources/policy.py
@@ -2,5 +2,6 @@
from shopify import mixins
import shopify
+
class Policy(ShopifyResource, mixins.Metafields, mixins.Events):
- pass
+ 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 e364ae07..cc16e3e3 100644
--- a/shopify/resources/product.py
+++ b/shopify/resources/product.py
@@ -4,7 +4,6 @@
class Product(ShopifyResource, mixins.Metafields, mixins.Events):
-
def price_range(self):
prices = [float(variant.price) for variant in self.variants]
f = "%0.2f"
@@ -28,5 +27,17 @@ def remove_from_collection(self, collection):
return collection.remove_product(self)
def add_variant(self, variant):
- variant.attributes['product_id'] = self.id
+ 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 ffda95b2..c94cac2d 100644
--- a/shopify/resources/recurring_application_charge.py
+++ b/shopify/resources/recurring_application_charge.py
@@ -1,6 +1,7 @@
from ..base import ShopifyResource
from .usage_charge import UsageCharge
+
def _get_first_by_status(resources, status):
for resource in resources:
if resource.status == status:
@@ -9,12 +10,11 @@ def _get_first_by_status(resources, status):
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))
+ self._load_attributes_from_response(self.put("customize", recurring_application_charge=kwargs))
@classmethod
def current(cls):
@@ -24,8 +24,5 @@ def current(cls):
"""
return _get_first_by_status(cls.find(), "active")
- def cancel(self):
- self._load_attributes_from_response(self.destroy)
-
def activate(self):
self._load_attributes_from_response(self.post("activate"))
diff --git a/shopify/resources/refund.py b/shopify/resources/refund.py
index 394887b3..124036b3 100644
--- a/shopify/resources/refund.py
+++ b/shopify/resources/refund.py
@@ -1,5 +1,29 @@
+import json
+
from ..base import ShopifyResource
class Refund(ShopifyResource):
- _prefix_source = "/admin/orders/$order_id/"
+ _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/shop.py b/shopify/resources/shop.py
index 00a927f8..4d447366 100644
--- a/shopify/resources/shop.py
+++ b/shopify/resources/shop.py
@@ -4,10 +4,9 @@
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 efea6347..802c3a7e 100644
--- a/shopify/resources/smart_collection.py
+++ b/shopify/resources/smart_collection.py
@@ -4,6 +4,5 @@
class SmartCollection(ShopifyResource, mixins.Metafields, mixins.Events):
-
def products(self):
return shopify.Product.find(collection_id=self.id)
diff --git a/shopify/resources/storefront_access_token.py b/shopify/resources/storefront_access_token.py
new file mode 100644
index 00000000..f1132f49
--- /dev/null
+++ b/shopify/resources/storefront_access_token.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class StorefrontAccessToken(ShopifyResource):
+ pass
diff --git a/shopify/resources/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
index 3555cfcf..bd5cd757 100644
--- a/shopify/resources/usage_charge.py
+++ b/shopify/resources/usage_charge.py
@@ -1,12 +1,13 @@
from ..base import ShopifyResource
+
class UsageCharge(ShopifyResource):
- _prefix_source = "/admin/recurring_application_charge/$recurring_application_charge_id/"
+ _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 "/admin/recurring_application_charges/%s" % (recurring_application_charge_id)
+ return "%s/recurring_application_charges/%s" % (cls.site, recurring_application_charge_id)
else:
- return "/admin"
+ 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 cc4f5485..743b071b 100644
--- a/shopify/resources/variant.py
+++ b/shopify/resources/variant.py
@@ -3,17 +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
+ 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 9097b8f5..561faacf 100644
--- a/shopify/session.py
+++ b/shopify/session.py
@@ -1,6 +1,8 @@
import time
import hmac
+import json
from hashlib import sha256
+
try:
import simplejson as json
except ImportError:
@@ -8,16 +10,20 @@
import re
from contextlib import contextmanager
from six.moves import urllib
+from shopify.api_access import ApiAccess
+from shopify.api_version import ApiVersion, Release, Unstable
import six
+
class ValidationException(Exception):
pass
+
class Session(object):
api_key = None
secret = None
- protocol = 'https'
- myshopify_domain = 'myshopify.com'
+ protocol = "https"
+ myshopify_domain = "myshopify.com"
port = None
@classmethod
@@ -27,55 +33,81 @@ def setup(cls, **kwargs):
@classmethod
@contextmanager
- def temp(cls, domain, token):
+ def temp(cls, domain, version, token):
import shopify
- original_site = shopify.ShopifyResource.get_site()
- original_token = shopify.ShopifyResource.get_headers().get('X-Shopify-Access-Token')
- original_session = shopify.Session(original_site, 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/oauth/authorize?%s" % (self.site, urllib.parse.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 ValidationException('Invalid HMAC: Possibly malicious login')
+ raise ValidationException("Invalid HMAC: Possibly malicious login")
- code = params['code']
+ code = params["code"]
- url = "%s/oauth/access_token?" % self.site
- query_params = dict(client_id=self.api_key, client_secret=self.secret, code=code)
- request = urllib.request.Request(url, urllib.parse.urlencode(query_params).encode('utf-8'))
+ 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().decode('utf-8'))['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
+ @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() == ""):
@@ -99,18 +131,18 @@ def validate_params(cls, params):
# Avoid replay attacks by making sure the request
# isn't more than a day old.
one_day = 24 * 60 * 60
- if int(params.get('timestamp', 0)) < time.time() - one_day:
+ if int(params.get("timestamp", 0)) < time.time() - one_day:
return False
return cls.validate_hmac(params)
@classmethod
def validate_hmac(cls, params):
- if 'hmac' not in params:
+ if "hmac" not in params:
return False
- hmac_calculated = cls.calculate_hmac(params).encode('utf-8')
- hmac_to_verify = params['hmac'].encode('utf-8')
+ hmac_calculated = cls.calculate_hmac(params).encode("utf-8")
+ hmac_to_verify = params["hmac"].encode("utf-8")
# Try to use compare_digest() to reduce vulnerability to timing attacks.
# If it's not available, just fall back to regular string comparison.
@@ -134,12 +166,20 @@ 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':
- # 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")
+ 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 8418bbc5..dfb0b4e4 100644
--- a/shopify/version.py
+++ b/shopify/version.py
@@ -1 +1 @@
-VERSION = '2.2.0'
+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 58e29b02..5ee53bbe 100644
--- a/test/article_test.py
+++ b/test/article_test.py
@@ -1,65 +1,75 @@
import shopify
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'))
+ 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'})
+ 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 ca9c60bb..19606a4b 100644
--- a/test/asset_test.py
+++ b/test/asset_test.py
@@ -1,41 +1,90 @@
+import base64
+
import shopify
from test.test_helper import TestCase
-class AssetTest(TestCase):
+class AssetTest(TestCase):
def test_get_assets(self):
- self.fake("assets", method='GET', body=self.load_fixture('assets'))
+ 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')
+ 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.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'})
+ 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)
+ 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_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.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'})
+ 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&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="{}")
+ 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 86ecafe5..5cc19a60 100644
--- a/test/base_test.py
+++ b/test/base_test.py
@@ -4,15 +4,18 @@
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"])
+
+ 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)
- 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_clear_session_should_clear_site_and_headers_from_base(self):
shopify.ShopifyResource.activate_session(self.session1)
shopify.ShopifyResource.clear_session()
@@ -36,50 +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.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:
- url = shopify.ShopifyResource._custom_method_collection_url('1', {})
- shopify.ShopifyResource.delete('1')
- mock.assert_called_with(url, {'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 728f2e18..e3a912f0 100644
--- a/test/blog_test.py
+++ b/test/blog_test.py
@@ -1,9 +1,15 @@
import shopify
from test.test_helper import TestCase
+
class BlogTest(TestCase):
-
def test_blog_creation(self):
- self.fake('blogs', method='POST', code=202, body=self.load_fixture('blog'), headers={'Content-type': 'application/json'})
- blog = shopify.Blog.create({'title': "Test Blog"})
+ self.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
index fbd96180..a5aea4f6 100644
--- a/test/carrier_service_test.py
+++ b/test/carrier_service_test.py
@@ -1,15 +1,21 @@
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'})
+ 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"})
+ 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'))
+ 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)
@@ -17,4 +23,4 @@ def test_get_carrier_service(self):
def test_set_format_attribute(self):
carrier_service = shopify.CarrierService()
carrier_service.format = "json"
- self.assertEqual("json", carrier_service.attributes['format'])
+ self.assertEqual("json", carrier_service.attributes["format"])
diff --git a/test/cart_test.py b/test/cart_test.py
index 3391dc9a..67836330 100644
--- a/test/cart_test.py
+++ b/test/cart_test.py
@@ -1,13 +1,13 @@
import shopify
from test.test_helper import TestCase
+
class CartTest(TestCase):
-
- def test_all_should_return_all_carts(self):
- self.fake('carts')
- carts = shopify.Cart.find()
- self.assertEqual(2, len(carts))
- self.assertEqual(2, carts[0].id)
- self.assertEqual("3eed8183d4281db6ea82ee2b8f23e9cc", carts[0].token)
- self.assertEqual(1, len(carts[0].line_items))
- self.assertEqual('test', carts[0].line_items[0].title)
+ 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
index 508a3d9f..5b21d560 100644
--- a/test/checkout_test.py
+++ b/test/checkout_test.py
@@ -1,12 +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))
+ 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 5c775af7..48873f15 100644
--- a/test/customer_saved_search_test.py
+++ b/test/customer_saved_search_test.py
@@ -1,23 +1,29 @@
import shopify
from test.test_helper import TestCase
+
class CustomerSavedSearchTest(TestCase):
-
def setUp(self):
super(CustomerSavedSearchTest, self).setUp()
self.load_customer_saved_search()
def test_get_customers_from_customer_saved_search(self):
- self.fake('customer_saved_searches/8899730/customers', body=self.load_fixture('customer_saved_search_customers'))
+ self.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
index baf7a055..04f2a0c8 100644
--- a/test/customer_test.py
+++ b/test/customer_test.py
@@ -1,29 +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'})
+ 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.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'])
+ self.assertEqual("newpass", customer.attributes["password"])
customer.save()
self.assertEqual("Bob", customer.first_name)
- self.assertEqual("newpass", customer.attributes['password'])
+ self.assertEqual("newpass", customer.attributes["password"])
def test_get_customer(self):
- self.fake('customers/207119551', method='GET', body=self.load_fixture('customer'))
- customer = shopify.Customer.find(207119551)
- self.assertEqual("Bob", customer.first_name)
+ 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'))
+ 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)
- 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/discount_test.py b/test/discount_test.py
deleted file mode 100644
index 063655c7..00000000
--- a/test/discount_test.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import shopify
-from test.test_helper import TestCase
-
-
-class DiscountTest(TestCase):
-
- def test_discount_creation(self):
- self.fake('discounts',
- method='POST',
- code=202,
- body=self.load_fixture('discount'),
- headers={'Content-type': 'application/json'})
- discount = shopify.Discount.create({
- "discount_type": "shipping",
- "code": "quidagis?",
- "starts_at": "2015-08-23T00:00:00-04:00",
- "ends_at": "2015-08-27T23:59:59-04:00",
- "usage_limit": 20
- })
- self.assertEqual("shipping", discount.discount_type)
- self.assertEqual("quidagis?", discount.code)
-
- def test_fetch_discounts(self):
- self.fake('discounts',
- method='GET',
- code=200,
- body=self.load_fixture('discounts'))
- discounts = shopify.Discount.find()
- self.assertEqual(2, len(discounts))
-
- def test_disable_discount(self):
- self.fake('discounts/992807812',
- method='GET',
- code=200,
- body=self.load_fixture('discount'))
- self.fake('discounts/992807812/disable',
- method='POST',
- code=200,
- body=self.load_fixture('discount_disabled'),
- headers={'Content-length': '0',
- 'Content-type': 'application/json'})
- discount = shopify.Discount.find(992807812)
- self.assertEqual("enabled", discount.status)
- discount.disable()
- self.assertEqual("disabled", discount.status)
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
index 15adbfe8..12a359bd 100644
--- a/test/draft_order_test.py
+++ b/test/draft_order_test.py
@@ -6,97 +6,145 @@
class DraftOrderTest(TestCase):
def setUp(self):
super(DraftOrderTest, self).setUp()
- self.fake('draft_orders/517119332', body=self.load_fixture('draft_order'))
+ 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', status=200, body=self.load_fixture('draft_order'))
+ 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', status=200, body=self.load_fixture('draft_orders'))
+ 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', status=200, body='{"count": 16}')
+ 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', status=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")))
+ 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', status=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.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', status=200, body=self.load_fixture('draft_order'), headers={'Content-type': 'application/json'})
+ 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'])
+ 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_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'})
+ 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)
+ 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_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.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)
+ 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.fake("draft_orders/517119332", method="DELETE", body="destroyed")
self.draft_order.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
+ self.assertEqual("DELETE", self.http.request.get_method())
def test_add_metafields_to_draft_order(self):
- self.fake('draft_orders/517119332/metafields', method='POST', status=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)
+ 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'))
+ 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)
+ 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.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)
+ 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/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/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_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/discount.json b/test/fixtures/discount.json
deleted file mode 100644
index e7b587be..00000000
--- a/test/fixtures/discount.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "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": "enabled",
- "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/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/discounts.json b/test/fixtures/discounts.json
deleted file mode 100644
index b796fdb5..00000000
--- a/test/fixtures/discounts.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "discounts": [
- {
- "id": 680866,
- "code": "TENOFF",
- "value": "10.0",
- "ends_at": null,
- "starts_at": null,
- "status": "enabled",
- "usage_limit": null,
- "minimum_order_amount": "0.00",
- "applies_to_id": null,
- "applies_once": false,
- "discount_type": "percentage",
- "applies_to_resource": null,
- "times_used": 1
- },
- {
- "id": 949676421,
- "code": "xyz",
- "value": "10.00",
- "ends_at": null,
- "starts_at": null,
- "status": "disabled",
- "usage_limit": null,
- "minimum_order_amount": "0.00",
- "applies_to_id": null,
- "applies_once": false,
- "discount_type": "fixed_amount",
- "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/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_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_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/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_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/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/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_charges_no_active.json b/test/fixtures/recurring_application_charges_no_active.json
index c806aa37..f3812604 100644
--- a/test/fixtures/recurring_application_charges_no_active.json
+++ b/test/fixtures/recurring_application_charges_no_active.json
@@ -35,4 +35,4 @@
"decorated_return_url": "http://yourapp.com?charge_id=455696195"
}
]
-}
\ No newline at end of file
+}
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
index f07b8ff2..877534a7 100644
--- a/test/fixtures/shipping_zones.json
+++ b/test/fixtures/shipping_zones.json
@@ -111,4 +111,4 @@
]
}
]
-}
\ No newline at end of file
+}
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/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 81296698..7519b3b9 100644
--- a/test/fulfillment_service_test.py
+++ b/test/fulfillment_service_test.py
@@ -1,15 +1,21 @@
import shopify
from test.test_helper import TestCase
+
class FulfillmentServiceTest(TestCase):
def test_create_new_fulfillment_service(self):
- self.fake("fulfillment_services", method='POST', body=self.load_fixture('fulfillment_service'), headers={'Content-type': 'application/json'})
+ 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)
@@ -17,4 +23,4 @@ def test_get_fulfillment_service(self):
def test_set_format_attribute(self):
fulfillment_service = shopify.FulfillmentService()
fulfillment_service.format = "json"
- self.assertEqual("json", fulfillment_service.attributes['format'])
+ self.assertEqual("json", fulfillment_service.attributes["format"])
diff --git a/test/fulfillment_test.py b/test/fulfillment_test.py
index 20034fb1..8bb84efb 100644
--- a/test/fulfillment_test.py
+++ b/test/fulfillment_test.py
@@ -2,41 +2,82 @@
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)
+ 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)
+ self.assertEqual("pending", fulfillment.status)
fulfillment.open()
- self.assertEqual('open', fulfillment.status)
+ self.assertEqual("open", fulfillment.status)
def test_able_to_complete_fulfillment(self):
fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
- success = self.load_fixture('fulfillment')
- success = success.replace(b'pending',b'success')
- self.fake("orders/450789469/fulfillments/255858046/complete", method='POST', headers={'Content-length':'0', 'Content-type': 'application/json'}, body=success)
+ 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(b'pending', b'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
index 0414cf61..2b5e6055 100644
--- a/test/gift_card_test.py
+++ b/test/gift_card_test.py
@@ -1,23 +1,65 @@
+from decimal import Decimal
import shopify
from test.test_helper import TestCase
-class GiftCardTest(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.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'))
+ 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'})
+ 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 1234898d..3dad3817 100644
--- a/test/image_test.py
+++ b/test/image_test.py
@@ -1,44 +1,81 @@
import shopify
from test.test_helper import TestCase
+import base64
-class ImageTest(TestCase):
+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})
+ 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(
+ "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'})
+ 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(
+ "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'))
+ 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'))
+ 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'))
+ 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 })
+ image = shopify.Image(attributes={"id": 850703190, "product_id": 632910392})
metafields = image.metafields()
self.assertEqual(1, len(metafields))
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/locations_test.py b/test/locations_test.py
deleted file mode 100644
index 44a6768c..00000000
--- a/test/locations_test.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import shopify
-from test.test_helper import TestCase
-
-class LocationsTest(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")
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 fb8b09b8..4ca6dbff 100644
--- a/test/order_risk_test.py
+++ b/test/order_risk_test.py
@@ -1,42 +1,52 @@
import shopify
from test.test_helper import TestCase
-class OrderRiskTest(TestCase):
- def test_create_order_risk(self):
- self.fake("orders/450789469/risks", method='POST', body= self.load_fixture('order_risk'), headers={'Content-type': 'application/json'})
- v = shopify.OrderRisk({'order_id':450789469})
- v.message = "This order was placed from a proxy IP"
- v.recommendation = "cancel"
- v.score = "1.0"
- v.source = "External"
- v.merchant_message = "This order was placed from a proxy IP"
- v.display = True
- v.cause_cancel = True
- v.save()
-
- self.assertEqual(284138680, v.id)
-
- def test_get_order_risks(self):
- self.fake("orders/450789469/risks", method='GET', body= self.load_fixture('order_risks'))
- v = shopify.OrderRisk.find(order_id=450789469)
- self.assertEqual(2, len(v))
-
- def test_get_order_risk(self):
- self.fake("orders/450789469/risks/284138680", method='GET', body= self.load_fixture('order_risk'))
- v = shopify.OrderRisk.find(284138680, order_id=450789469)
- self.assertEqual(284138680, v.id)
-
- def test_delete_order_risk(self):
- self.fake("orders/450789469/risks/284138680", method='GET', body= self.load_fixture('order_risk'))
- self.fake("orders/450789469/risks/284138680", method='DELETE', body="destroyed")
- v = shopify.OrderRisk.find(284138680, order_id=450789469)
- v.destroy()
-
- def test_delete_order_risk(self):
- self.fake("orders/450789469/risks/284138680", method='GET', body= self.load_fixture('order_risk'))
- self.fake("orders/450789469/risks/284138680", method='PUT', body= self.load_fixture('order_risk'), headers={'Content-type': 'application/json'})
-
- v = shopify.OrderRisk.find(284138680, order_id=450789469)
- v.position = 3
- v.save()
+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 d70645c4..257ab08a 100644
--- a/test/order_test.py
+++ b/test/order_test.py
@@ -3,8 +3,8 @@
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 dcc9ae72..de183691 100644
--- a/test/product_test.py
+++ b/test/product_test.py
@@ -1,18 +1,28 @@
import shopify
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'})
-
- field = self.product.add_metafield(shopify.Metafield({'namespace': "contact", 'key': "email", 'value': "123@example.com", 'value_type': "string"}))
+ self.fake(
+ "products/632910392/metafields",
+ method="POST",
+ code=201,
+ body=self.load_fixture("metafield"),
+ headers={"Content-type": "application/json"},
+ )
+
+ field = self.product.add_metafield(
+ shopify.Metafield(
+ {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"}
+ )
+ )
self.assertFalse(field.is_new())
self.assertEqual("contact", field.namespace)
@@ -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()
@@ -29,7 +39,7 @@ def test_get_metafields_for_product(self):
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'))
+ 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))
@@ -37,26 +47,41 @@ def test_get_metafields_for_product_with_params(self):
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'))
+ 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'))
+ 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'})
+ 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 af5fcfb2..c785388e 100644
--- a/test/recurring_charge_test.py
+++ b/test/recurring_charge_test.py
@@ -1,11 +1,17 @@
import shopify
from test.test_helper import TestCase
+
class RecurringApplicationChargeTest(TestCase):
def test_activate_charge(self):
# Just check that calling activate doesn't raise an exception.
- self.fake("recurring_application_charges/35463/activate", method='POST',headers={'Content-length':'0', 'Content-type': 'application/json'}, body=" ")
- charge = shopify.RecurringApplicationCharge({'id': 35463})
+ 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):
@@ -26,7 +32,11 @@ 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'))
+ 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)
@@ -35,6 +45,19 @@ def test_customize_method_increases_capped_amount(self):
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.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
index f7a47851..905bbabc 100644
--- a/test/refund_test.py
+++ b/test/refund_test.py
@@ -1,11 +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'))
+ 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 11843810..8d73e293 100644
--- a/test/session_test.py
+++ b/test/session_test.py
@@ -6,35 +6,41 @@
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_ignore_everything_but_the_subdomain_in_the_shop(self):
- session = shopify.Session("http://user:pass@testshop.notshopify.net/path", "any-token")
- self.assertEqual("https://testshop.myshopify.com/admin", session.site)
+ 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", "any-token")
- self.assertEqual("https://testshop.myshopify.com/admin", session.site)
-
- def test_not_raise_error_without_params(self):
- session = shopify.Session("testshop.myshopify.com", "any-token")
+ session = shopify.Session("testshop", "unstable", "any-token")
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", session.site)
def test_raise_error_if_params_passed_but_signature_omitted(self):
with self.assertRaises(shopify.ValidationException):
- session = shopify.Session("testshop.myshopify.com")
- token = session.request_token({'code':'any_code', 'foo': 'bar', 'timestamp':'1234'})
+ 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")
@@ -42,31 +48,31 @@ 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_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', 'token1')
+ session = shopify.Session("fakeshop.localhost:3000", "unstable", "token1")
shopify.ShopifyResource.activate_session(session)
- self.assertEqual('https://fakeshop.localhost:3000/admin', shopify.ShopifyResource.site)
+ self.assertEqual("https://fakeshop.localhost:3000/admin/api/unstable", shopify.ShopifyResource.site)
- session = shopify.Session('fakeshop', 'token1')
+ session = shopify.Session("fakeshop", "unstable", "token1")
shopify.ShopifyResource.activate_session(session)
- self.assertEqual('https://fakeshop.localhost:3000/admin', shopify.ShopifyResource.site)
+ self.assertEqual("https://fakeshop.localhost:3000/admin/api/unstable", shopify.ShopifyResource.site)
finally:
shopify.Session.setup(myshopify_domain="myshopify.com", port=None)
@@ -74,137 +80,242 @@ 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://None/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?client_id=My_test_key&scope=write_products", self.normalize_url(permission_url))
-
- def test_create_permission_url_returns_correct_url_with_single_scope_and_redirect_uri(self):
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ permission_url = session.create_permission_url("my_redirect_uri.com")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_scope(self):
shopify.Session.setup(api_key="My_test_key", secret="My test secret")
- session = shopify.Session('http://localhost.myshopify.com')
+ 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?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products", self.normalize_url(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?client_id=My_test_key&scope=write_products%2Cwrite_customers", self.normalize_url(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_redirect_uri_and_empty_scope(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ scope = []
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+ self.normalize_url(permission_url),
+ )
- def test_create_permission_url_returns_correct_url_with_no_scope_no_redirect_uri(self):
+ 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')
+ 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?client_id=My_test_key&scope=", self.normalize_url(permission_url))
+ 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(shopify.ValidationException):
- session.request_token({'code':'any-code', 'timestamp':'1234'})
+ 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)
+ 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'
+ shopify.Session.secret = "hush"
params = {
- 'shop': 'some-shop.myshopify.com',
- 'code': 'a94a110d86d2452eb3e2af4cfb8a3828',
- 'timestamp': '1337178173',
- 'hmac': '2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2',
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "timestamp": "1337178173",
+ "hmac": "2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2",
}
- self.assertEqual(shopify.Session.calculate_hmac(params), params['hmac'])
+ 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' }
+ 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()
+ 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'
+ shopify.Session.secret = "hush"
params = {
- 'shop': 'some-shop.myshopify.com',
- 'code': 'a94a110d86d2452eb3e2af4cfb8a3828',
- 'timestamp': '1337178173',
- 'hmac': u('2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2'),
+ "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'
+ shopify.Session.secret = "hush"
params = {
- 'shop': 'some-shop.myshopify.com',
- 'code': 'a94a110d86d2452eb3e2af4cfb8a3828',
- 'hmac': u('2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2'),
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "hmac": u("2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2"),
}
self.assertFalse(shopify.Session.validate_params(params))
- def test_return_token_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
+ 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))
- 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')
+ 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'
+ 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')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
session = session.request_token(params)
def test_raise_error_if_hmac_does_not_match_expected(self):
- shopify.Session.secret='secret'
- params = {'foo': 'hello', 'timestamp': time.time()}
+ 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'
+ params["hmac"] = hmac
+ params["bar"] = "world"
+ params["code"] = "code"
with self.assertRaises(shopify.ValidationException):
- session = shopify.Session('http://localhost.myshopify.com')
+ 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)}
+ params = {"code": "any-code", "timestamp": time.time() - (2 * one_day)}
hmac = shopify.Session.calculate_hmac(params)
- params['hmac'] = hmac
+ params["hmac"] = hmac
with self.assertRaises(shopify.ValidationException):
- session = shopify.Session('http://localhost.myshopify.com')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
session = session.request_token(params)
+ def test_access_scopes_are_nil_by_default(self):
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
+ self.assertIsNone(session.access_scopes)
+
+ def test_access_scopes_when_valid_scopes_passed_in(self):
+ session = shopify.Session(
+ shop_url="testshop.myshopify.com",
+ version="unstable",
+ token="any-token",
+ access_scopes="read_products, write_orders",
+ )
+
+ expected_access_scopes = shopify.ApiAccess("read_products, write_orders")
+ self.assertEqual(expected_access_scopes, session.access_scopes)
+
+ def test_access_scopes_set_with_api_access_object_passed_in(self):
+ session = shopify.Session(
+ shop_url="testshop.myshopify.com",
+ version="unstable",
+ token="any-token",
+ access_scopes=shopify.ApiAccess("read_products, write_orders"),
+ )
+
+ expected_access_scopes = shopify.ApiAccess("read_products, write_orders")
+ self.assertEqual(expected_access_scopes, session.access_scopes)
+
def normalize_url(self, url):
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
query = "&".join(sorted(query.split("&")))
return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
+
+ def test_session_with_coerced_version(self):
+ future_version = "2030-01"
+ session = shopify.Session("test.myshopify.com", future_version, "token")
+ self.assertEqual(session.api_version.name, future_version)
+ self.assertEqual(
+ session.api_version.api_path("https://test.myshopify.com"),
+ f"https://test.myshopify.com/admin/api/{future_version}",
+ )
+
+ def test_session_with_invalid_version(self):
+ with self.assertRaises(shopify.VersionNotFoundError):
+ shopify.Session("test.myshopify.com", "invalid-version", "token")
diff --git a/test/session_token_test.py b/test/session_token_test.py
new file mode 100644
index 00000000..0df7147f
--- /dev/null
+++ b/test/session_token_test.py
@@ -0,0 +1,110 @@
+from shopify import session_token
+from test.test_helper import TestCase
+from datetime import datetime, timedelta
+
+import jwt
+import sys
+
+if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0
+ import time
+
+
+def timestamp(date):
+ return time.mktime(date.timetuple()) if sys.version_info[0] < 3 else date.timestamp()
+
+
+class TestSessionTokenGetDecodedSessionToken(TestCase):
+ @classmethod
+ def setUpClass(self):
+ self.secret = "API Secret"
+ self.api_key = "API key"
+
+ @classmethod
+ def setUp(self):
+ current_time = datetime.now()
+ self.payload = {
+ "iss": "https://test-shop.myshopify.com/admin",
+ "dest": "https://test-shop.myshopify.com",
+ "aud": self.api_key,
+ "sub": "1",
+ "exp": timestamp((current_time + timedelta(0, 60))),
+ "nbf": timestamp(current_time),
+ "iat": timestamp(current_time),
+ "jti": "4321",
+ "sid": "abc123",
+ }
+
+ @classmethod
+ def build_auth_header(self):
+ mock_session_token = jwt.encode(self.payload, self.secret, algorithm="HS256")
+ return "Bearer {session_token}".format(session_token=mock_session_token)
+
+ def test_raises_if_token_authentication_header_is_not_bearer(self):
+ authorization_header = "Bad auth header"
+
+ with self.assertRaises(session_token.TokenAuthenticationError) as cm:
+ session_token.decode_from_header(authorization_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token", str(cm.exception))
+
+ def test_raises_jwt_error_if_session_token_is_expired(self):
+ self.payload["exp"] = timestamp((datetime.now() + timedelta(0, -11)))
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Signature has expired", str(cm.exception))
+
+ def test_raises_jwt_error_if_invalid_alg(self):
+ bad_session_token = jwt.encode(self.payload, None, algorithm="none")
+ invalid_header = "Bearer {session_token}".format(session_token=bad_session_token)
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(invalid_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The specified alg value is not allowed", str(cm.exception))
+
+ def test_raises_jwt_error_if_invalid_signature(self):
+ bad_session_token = jwt.encode(self.payload, "bad_secret", algorithm="HS256")
+ invalid_header = "Bearer {session_token}".format(session_token=bad_session_token)
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(invalid_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Signature verification failed", str(cm.exception))
+
+ def test_raises_if_aud_doesnt_match_api_key(self):
+ self.payload["aud"] = "bad audience"
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Audience doesn't match", str(cm.exception))
+
+ def test_raises_if_issuer_hostname_is_invalid(self):
+ self.payload["iss"] = "bad_shop_hostname"
+
+ with self.assertRaises(session_token.InvalidIssuerError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Invalid issuer", str(cm.exception))
+
+ def test_raises_if_iss_and_dest_dont_match(self):
+ self.payload["dest"] = "bad_shop.myshopify.com"
+
+ with self.assertRaises(session_token.MismatchedHostsError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The issuer and destination do not match", str(cm.exception))
+
+ def test_returns_decoded_payload(self):
+ decoded_payload = session_token.decode_from_header(
+ self.build_auth_header(), api_key=self.api_key, secret=self.secret
+ )
+
+ self.assertEqual(self.payload, decoded_payload)
+
+ def test_allow_10_seconds_clock_skew_in_nbf(self):
+ self.payload["nbf"] = timestamp((datetime.now() + timedelta(seconds=10)))
+
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
diff --git a/test/shipping_zone_test.py b/test/shipping_zone_test.py
index e81cfe6a..3d1e1e4d 100644
--- a/test/shipping_zone_test.py
+++ b/test/shipping_zone_test.py
@@ -1,11 +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'))
+ 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))
-
+ 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 2f02a632..3a88a2a6 100644
--- a/test/shop_test.py
+++ b/test/shop_test.py
@@ -1,6 +1,7 @@
import shopify
from test.test_helper import TestCase
+
class ShopTest(TestCase):
def setUp(self):
super(ShopTest, self).setUp()
@@ -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 2d9db66d..666ac792 100644
--- a/test/test_helper.py
+++ b/test/test_helper.py
@@ -5,53 +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 = "https://this-is-my-test-show.myshopify.com/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://this-is-my-test-show.myshopify.com'
+ self.http.set_response(Exception("Bad request"))
+ self.http.site = "https://this-is-my-test-show.myshopify.com"
- def load_fixture(self, name, format='json'):
- with open(os.path.dirname(__file__)+'/fixtures/%s.%s' % (name, format), 'rb') as f:
+ 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')
+ 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']):
+ if "extension" in kwargs and not kwargs["extension"]:
extension = ""
else:
- extension = ".%s" % (kwargs.pop('extension', 'json'))
+ extension = ".%s" % (kwargs.pop("extension", "json"))
- url = "https://this-is-my-test-show.myshopify.com/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 79335eee..e02fae00 100644
--- a/test/transaction_test.py
+++ b/test/transaction_test.py
@@ -1,10 +1,11 @@
import shopify
from test.test_helper import TestCase
+
class TransactionTest(TestCase):
def setUp(self):
super(TransactionTest, self).setUp()
- self.fake("orders/450789469/transactions/389404469", method='GET', body=self.load_fixture('transaction'))
+ 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
index ab341fb3..a816636b 100644
--- a/test/usage_charge_test.py
+++ b/test/usage_charge_test.py
@@ -1,16 +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'})
+ 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 = shopify.UsageCharge(
+ {"price": 9.0, "description": "1000 emails", "recurring_application_charge_id": 654381177}
+ )
charge.save()
- self.assertEqual('1000 emails', charge.description)
+ 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'))
+ 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)
+ 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 ebfdd8c0..63ecb639 100644
--- a/test/variant_test.py
+++ b/test/variant_test.py
@@ -1,34 +1,49 @@
import shopify
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="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'})
+ 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})
+ 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'})
+ 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
index a2f77bd6..1523475c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,12 @@
[tox]
-envlist = py27, py34, py35
+envlist = py27, py34, py35, py36, py38, py39
+skip_missing_interpreters = true
[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/shopify
-commands = python setup.py test
+commands=
+ python setup.py test
[testenv:flake8]
basepython=python