Skip to content

Commit

Permalink
feat!: migration to v9 api version (#303)
Browse files Browse the repository at this point in the history
Co-authored-by: Ola Herrdahl <olaherrdahl@users.noreply.github.com>
Co-authored-by: abk16 <and.theunnamed@gmail.com>
  • Loading branch information
3 people authored Jun 3, 2024
1 parent 6f22556 commit b9aff61
Show file tree
Hide file tree
Showing 19 changed files with 416 additions and 159 deletions.
15 changes: 12 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ Any contribution are welcomed.

For submitting PRs, they need to have test coverage which pass the full run in Travis CI.

## Developing

If you want to run the toggl CLI during development, I recommend you to use flow where you `pip install -e .`, which
symlinks locally the package and then you can simply use the CLI like `toggl ls`.

Also, if you find yourself with non-descriptive exception, you can set env. variable `export TOGGL_EXCEPTIONS=1` which
then will give you then the full stack trace.

## Tests

For running integration tests you need dummy account on Toggl, where **you don't have any important data** as the data
will be messed up with and eventually **deleted**! Get API token for this test account and set it as an environmental variable
***TOGGL_API_TOKEN***.
`TOGGL_API_TOKEN`. Also figure out the Workspace ID of your account (`toggl workspace ls`) and set is as `TOGGL_WORKSPACE`
environmental variable.

There are two sets of integration tests: normal and premium. To be able to run the premium set you have to have payed
workspace. As this is quiet unlikely you can leave the testing on Travis CI as it runs also the premium tests set.
Expand All @@ -20,8 +29,8 @@ Tests are written using `pytest` framework and are split into three categories (

## Running tests

In order to run tests first you need to have required packages installed. You can install them using `pip install togglCli[test]` or
`python setup.py test`.
In order to run tests first you need to have required packages installed. You can install them using `pip install togglCli[test]`,
`python setup.py test` or `pip install -r test-requirements.txt`.

By default unit and integration tests are run without the one testing premium functionality, as most probably you don't have access to Premium workspace for testing purposes.
If you want to run just specific category you can do so using for example`pytest -m unit` for only unit tests.
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ addopts = --cov toggl -m "not premium" --maxfail=20
markers =
unit: Unit tests testing framework. No outside dependencies (no out-going requests)
integration: Integration tests which tests end to end coherence of API wrapper. Requires connectivity to Toggl API.
premium: Subcategory of Integration tests that requires to have Premium/Paid workspace for the tests.
premium: Subcategory of Integration tests that requires to have Premium/Paid workspace for the tests.
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
setuptools==69.0.3
pendulum==3.0.0
requests>=2.23.0
click==8.1.3
inquirer==2.9.1
click==8.1.7
inquirer==3.2.4
prettytable==3.6.0
validate_email==1.3
click-completion==0.5.2
pbr==5.8.0
pbr==6.0.0
notify-py==0.3.42
3 changes: 1 addition & 2 deletions tests/configs/non-premium.config
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ version = 2.0.0

[options]
tz = utc
default_wid = 3057440

default_wid = 8379305
2 changes: 1 addition & 1 deletion tests/configs/premium.config
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ version = 2.0.0

[options]
tz = utc
default_wid = 2609276
default_wid = 8379305
14 changes: 10 additions & 4 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,13 @@ class Cleanup:
def _ids_cleanup(base, config=None, batch=False, *ids):
config = config or get_config()

wid = config.default_workspace.id
workspace_url = '/workspaces/{}'.format(wid)
if batch:
utils.toggl('/{}/{}'.format(base, ','.join([str(eid) for eid in ids])), 'delete', config=config)
utils.toggl('{}/{}/{}'.format(workspace_url, base, ','.join([str(eid) for eid in ids])), 'delete', config=config)
else:
for entity_id in ids:
utils.toggl('/{}/{}'.format(base, entity_id), 'delete', config=config)
utils.toggl('{}/{}/{}'.format(workspace_url, base, entity_id), 'delete', config=config)

@staticmethod
def _all_cleanup(cls, config=None):
Expand Down Expand Up @@ -171,12 +173,16 @@ def time_entries(config=None, *ids):
if not ids:
config = config or get_config()
entities = api.TimeEntry.objects.all(config=config)
current_entry = api.TimeEntry.objects.current(config=config)
if current_entry is not None:
current_entry.stop_and_save()
entities.append(current_entry)
ids = [entity.id for entity in entities]

if not ids:
return

Cleanup._ids_cleanup('time_entries', config, True, *ids)
Cleanup._ids_cleanup('time_entries', config, False, *ids)

@staticmethod
def project_users(config=None, *ids):
Expand All @@ -195,7 +201,7 @@ def projects(config=None, *ids):
if not ids:
return

Cleanup._ids_cleanup('projects', config, True, *ids)
Cleanup._ids_cleanup('projects', config, False, *ids)

@staticmethod
def tasks(config=None, *ids):
Expand Down
1 change: 0 additions & 1 deletion tests/integration/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class Meta:
# client = factory.SubFactory(ClientFactory)
active = True
is_private = True
billable = False


class TaskFactory(TogglFactory):
Expand Down
26 changes: 13 additions & 13 deletions tests/integration/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ def test_add_full_non_premium(self, cmd, fake, factories, config):
assert result.obj.exit_code == 0
assert Project.objects.get(result.created_id(), config=config).client == client

result = cmd('projects add --name \'{}\' --private --color 2'.format(fake.word()))
result = cmd('projects add --name \'{}\' --private --color #c9806b'.format(fake.word()))
assert result.obj.exit_code == 0

prj = Project.objects.get(result.created_id(), config=config) # type: Project
assert prj.is_private is True
assert prj.color == 2
assert prj.color == '#c9806b'

with pytest.raises(exceptions.TogglPremiumException):
cmd('projects add --name \'{}\' --billable'.format(fake.word()))
Expand All @@ -52,49 +52,49 @@ def test_add_full_premium(self, cmd, fake):
def test_get(self, cmd, fake, factories):
name = fake.word()
client = factories.ClientFactory()
project = factories.ProjectFactory(name=name, is_private=False, color=2, client=client)
project = factories.ProjectFactory(name=name, is_private=False, color='#c9806b', client=client)

result = cmd('projects get \'{}\''.format(project.id))
id_parsed = result.parse_detail()

assert id_parsed['name'] == name
assert id_parsed['billable'] == 'False'
assert id_parsed['auto_estimates'] == 'False'
assert not bool(id_parsed['billable'])
assert not bool(id_parsed['auto_estimates'])
assert id_parsed['active'] == 'True'
assert id_parsed['is_private'] == 'False'
assert id_parsed['color'] == '2'
assert id_parsed['color'] == '#c9806b'
assert str(client.id) in id_parsed['client']

result = cmd('projects get \'{}\''.format(name))
name_parsed = result.parse_detail()

assert name_parsed['name'] == name
assert name_parsed['billable'] == 'False'
assert name_parsed['auto_estimates'] == 'False'
assert not bool(name_parsed['billable'])
assert not bool(name_parsed['auto_estimates'])
assert name_parsed['active'] == 'True'
assert name_parsed['is_private'] == 'False'
assert name_parsed['color'] == '2'
assert name_parsed['color'] == '#c9806b'
assert str(client.id) in name_parsed['client']

def test_update(self, cmd, fake, config, factories):
name = fake.name()
project = factories.ProjectFactory(name=name, is_private=False, color=2)
project = factories.ProjectFactory(name=name, is_private=False, color='#c9806b')

new_name = fake.name()
new_client = factories.ClientFactory()
result = cmd('projects update --name \'{}\' --client \'{}\' --private --color 1 \'{}\''.format(new_name, new_client.name, name))
result = cmd('projects update --name \'{}\' --client \'{}\' --private --color #0b83d9 \'{}\''.format(new_name, new_client.name, name))
assert result.obj.exit_code == 0

project_obj = Project.objects.get(project.id, config=config)
assert project_obj.name == new_name
assert project_obj.client == new_client
assert project_obj.color == 1
assert project_obj.color == '#0b83d9'
assert project_obj.is_private is True

@pytest.mark.premium
def test_update_premium(self, cmd, fake, config, factories):
name = fake.name()
project = factories.ProjectFactory(name=name, is_private=False, color=2)
project = factories.ProjectFactory(name=name, is_private=False, color='#c9806b')

result = cmd('projects update --billable --rate 10.10 --auto-estimates \'{}\''.format(name))
assert result.obj.exit_code == 0
Expand Down
14 changes: 11 additions & 3 deletions tests/integration/test_time_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_add_basic(self, cmd, fake, config):
assert result.obj.exit_code == 0

entry = TimeEntry.objects.get(result.created_id(), config=config) # type: TimeEntry
assert entry.start == start
assert entry.start == start.replace(microsecond=0)
assert (entry.stop - entry.start).seconds == 3722

def test_add_tags(self, cmd, fake, config):
Expand Down Expand Up @@ -179,8 +179,16 @@ def test_now(self, cmd, config, factories):
def test_continue(self, cmd, config, factories):
some_entry = factories.TimeEntryFactory()

start = pendulum.now('utc')
stop = start + pendulum.duration(seconds=10)
# Stop and remove any running and recent time entries first
pre_running_entry = TimeEntry.objects.current(config=config)
if pre_running_entry is not None:
pre_running_entry.stop_and_save()
recent_entries = TimeEntry.objects.filter(order="desc", config=config, start=pendulum.now('utc') - pendulum.duration(minutes=2), stop=pendulum.now('utc'))
for to_delete_entry in recent_entries:
to_delete_entry.delete(config=config)

stop = pendulum.now('utc') - pendulum.duration(seconds=1)
start = stop - pendulum.duration(seconds=10)
last_entry = factories.TimeEntryFactory(start=start, stop=stop)

result = cmd('continue')
Expand Down
23 changes: 11 additions & 12 deletions tests/unit/api/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@


class RandomEntity(base.TogglEntity):
_endpoints_name = 'random_entities'

some_field = fields.StringField()


Expand Down Expand Up @@ -149,14 +151,14 @@ def test_unbound_set(self):
tset.filter()

with pytest.raises(exceptions.TogglException):
tset.base_url
tset.entity_endpoints_name

def test_url(self):
tset = base.TogglSet(url='http://some-url.com')
assert tset.base_url == 'http://some-url.com'
assert tset.entity_endpoints_name == 'http://some-url.com'

tset = base.TogglSet(RandomEntity)
assert tset.base_url == 'random_entitys'
assert tset.entity_endpoints_name == 'random_entities'

def test_can_get_detail(self):
tset = base.TogglSet(can_get_detail=False)
Expand Down Expand Up @@ -189,9 +191,8 @@ def test_can_get_list(self):
def test_get_detail_basic(self, mocker):
mocker.patch.object(utils, 'toggl')
utils.toggl.return_value = {
'data': {
'some_field': 'asdf'
}
'id': 123,
'some_field': 'asdf'
}

tset = base.TogglSet(RandomEntity)
Expand All @@ -202,9 +203,7 @@ def test_get_detail_basic(self, mocker):

def test_get_detail_none(self, mocker):
mocker.patch.object(utils, 'toggl')
utils.toggl.return_value = {
'data': None
}
utils.toggl.return_value = None

tset = base.TogglSet(RandomEntity)
obj = tset.get(id=123)
Expand Down Expand Up @@ -477,6 +476,8 @@ class ExtendedMetaTestEntityWithConflicts(MetaTestEntity):
## TogglEntity

class Entity(base.TogglEntity):
_endpoints_name = "entities"

string = fields.StringField()
integer = fields.IntegerField()
boolean = fields.BooleanField()
Expand Down Expand Up @@ -616,9 +617,7 @@ def test_copy(self):
def test_save_create(self, mocker):
mocker.patch.object(utils, 'toggl')
utils.toggl.return_value = {
'data': {
'id': 333
}
'id': 333
}

obj = Entity(string='asd', integer=123)
Expand Down
14 changes: 13 additions & 1 deletion toggl/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
from toggl.api.models import Client, Workspace, Project, User, WorkspaceUser, ProjectUser, TimeEntry, Task, Tag
from toggl.api.models import (
Client,
Workspace,
Project,
User,
WorkspaceUser,
ProjectUser,
TimeEntry,
Task,
Tag,
InvitationResult,
Organization,
)
Loading

0 comments on commit b9aff61

Please sign in to comment.