feat: grounds-up rewrite + leverage existing sdks + remove cruft
This commit is contained in:
parent
add064e7c9
commit
23ef43e6b2
16 changed files with 297 additions and 476 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,8 +4,9 @@ __pycache__/
|
|||
*$py.class
|
||||
*.venv
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
*.so
|
||||
config.yml
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
|
|
33
README.md
33
README.md
|
@ -1,10 +1,6 @@
|
|||
# slack-status-cli
|
||||
:sparkle: Tooling to set your Slack status on the fly without having to click around
|
||||
# slck
|
||||
|
||||
[![CICD](https://github.com/mcataford/slack-status-cli/actions/workflows/main.yml/badge.svg)](https://github.com/mcataford/slack-status-cli/actions/workflows/main.yml)
|
||||
[![codecov](https://codecov.io/gh/mcataford/slack-status-cli/branch/main/graph/badge.svg?token=10VP1ZDBHR)](https://codecov.io/gh/mcataford/slack-status-cli)
|
||||
[![python-support](https://img.shields.io/badge/python-%5E3.7-brightgreen)]()
|
||||
[![latest-release](https://img.shields.io/github/v/release/mcataford/slack-status-cli?include_prereleases&label=latest%20release&sort=semver)]()
|
||||
[![python-support](https://img.shields.io/badge/python-%5E3.12-brightgreen)]()
|
||||
|
||||
## Overview
|
||||
|
||||
|
@ -12,28 +8,23 @@ Clicking around Slack to update statuses is not only annoying, but if you use st
|
|||
to broadcast what you are up to when jumping into new things, you quickly find yourself spending minutes of you day
|
||||
clicking around and setting the same statuses over and over again since the UI isn't great at remembering them.
|
||||
|
||||
Enter `slack-status-cli`. With it, you can set statuses (with or without expiration dates) without leaving the terminal.
|
||||
Enter `slck`. With it, you can set statuses (with or without expiration dates) without leaving the terminal.
|
||||
More importantly, you can also set presets and defaults to save time on statuses you reuse all the time.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can use `slack-status-cli` without a configuration file and provide everything via arguments (see `slack-status-cli
|
||||
-h` for the list of flags you can pass in), or set up a file under `~/.config/slack-status-cli` that follows the format:
|
||||
A configuration YAML file can be used to set up credentials and presets:
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": {
|
||||
"pairing": { "text": "Pairing", "icon": ":pear:" }
|
||||
},
|
||||
"defaults": { "duration": "1h", "icon": ":calendar:" }
|
||||
}
|
||||
```yaml
|
||||
token: <slack-token>
|
||||
presets:
|
||||
<preset-label>:
|
||||
text: ...
|
||||
emoji: ...
|
||||
```
|
||||
|
||||
`presets` allows you to set up a map of labels (used to select the preset) to values (defining the status text, icon and
|
||||
duration), `defaults` allows you to set up sane defaults used in all statuses if the specified fields are not provided
|
||||
(in the above, all statuses would have a duration of one hour if not specified, and a default :calendar: icon -- presets
|
||||
and/or CLI args will override these defaults if given).
|
||||
`presets` allows you to set up a map of labels (used to select the preset) to values (defining the status text, emoji).
|
||||
|
||||
## Installation
|
||||
|
||||
You can clone this repository and build from source or use pre-built artifacts. To build yourself, you can `. script/bootstrap && python -m build`. Build artifacts for released versions are also available under [releases](https://github.com/mcataford/slack-status-cli/releases).
|
||||
The best way to enjoy this is via pipx: `pipx install git+https://forge.karnov.club/marc/slck`.
|
||||
|
|
97
main.py
Normal file
97
main.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
"""
|
||||
slck: Slack Status CLI
|
||||
|
||||
Facilitates setting Slack status text/emojis via the
|
||||
command-line.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
import pydantic
|
||||
import slack_sdk
|
||||
import yaml
|
||||
|
||||
|
||||
class ProfilePayload(pydantic.BaseModel):
|
||||
"""Profile payload sent to Slack's API."""
|
||||
|
||||
status_text: str = ""
|
||||
status_emoji: str = ""
|
||||
status_expiration: int = 0
|
||||
|
||||
|
||||
class Preset(pydantic.BaseModel):
|
||||
"""Represents a set of value used as a preset."""
|
||||
|
||||
text: str
|
||||
emoji: str = ""
|
||||
|
||||
|
||||
class Configuration(pydantic.BaseModel):
|
||||
"""Tool configuration."""
|
||||
|
||||
token: str
|
||||
presets: dict[str, Preset]
|
||||
|
||||
|
||||
def get_client(token: str) -> slack_sdk.WebClient:
|
||||
"""Returns an authenticated API client."""
|
||||
return slack_sdk.WebClient(token=token)
|
||||
|
||||
|
||||
def get_configuration(path: str) -> Configuration:
|
||||
"""Loads configuration from file."""
|
||||
conf_path = pathlib.Path(path)
|
||||
|
||||
if not conf_path.exists():
|
||||
raise RuntimeError(f"Configuration file not found: {path}")
|
||||
|
||||
with open(conf_path, "r", encoding="utf8") as conf_file:
|
||||
return Configuration(**yaml.safe_load(conf_file))
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--text", "-t", help="Status text.")
|
||||
@click.option("--emoji", "-e", help="Emoji attached to the status.")
|
||||
@click.option("--preset", "-p", help="Preset for text/emoji combinations.")
|
||||
@click.option("--config", "-c", "config_path", help="Path to configuration.")
|
||||
def cli(
|
||||
*, text: str = None, emoji: str = None, preset: str = None, config_path: str = None
|
||||
):
|
||||
if text is None and preset is None:
|
||||
raise RuntimeError(
|
||||
"Must specify either status text via --text/-t or a preset via --preset/-p."
|
||||
)
|
||||
|
||||
conf = get_configuration(config_path)
|
||||
|
||||
status_text, status_emoji = None, None
|
||||
|
||||
if preset is not None and preset in conf.presets:
|
||||
preset_data = conf.presets[preset]
|
||||
status_text = preset_data.text if preset_data.text else status_text
|
||||
status_emoji = preset_data.emoji if preset_data.emoji else status_emoji
|
||||
elif preset is not None:
|
||||
raise RuntimeError(f"Unknown preset: {preset}")
|
||||
|
||||
if text is not None:
|
||||
status_text = text
|
||||
|
||||
if emoji is not None:
|
||||
status_emoji = emoji
|
||||
|
||||
payload = ProfilePayload(status_text=status_text, status_emoji=status_emoji)
|
||||
client = get_client(conf.token)
|
||||
api_response = client.users_profile_set(profile=payload.model_dump())
|
||||
|
||||
if not api_response.get("ok", False):
|
||||
raise RuntimeError("Failed to set status!")
|
||||
|
||||
|
||||
def run():
|
||||
"""Entrypoint."""
|
||||
try:
|
||||
cli()
|
||||
except Exception as e:
|
||||
click.echo(e)
|
|
@ -1,29 +1,28 @@
|
|||
[project]
|
||||
name = "slack-status-cli"
|
||||
name = "slck"
|
||||
version = "0.1.0"
|
||||
description = "Tooling to set your Slack status on the fly without having to click around"
|
||||
requires-python = ">=3.8"
|
||||
readme = "README.md"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"click~=8.0",
|
||||
"pydantic~=2.0",
|
||||
"slack_sdk~=3.0",
|
||||
"pyyaml~=6.0"
|
||||
]
|
||||
requires-python = "~= 3.12"
|
||||
|
||||
[[project.authors]]
|
||||
name = "Marc Cataford"
|
||||
email = "mcat@riseup.net"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/mcataford/slack-status-cli"
|
||||
"Bug Tracker" = "https://github.com/mcataford/slack-status-cli/issues"
|
||||
Homepage = "https://forge.karnov.club/marc/slck"
|
||||
"Bug Tracker" = "https://forge.karnov.club/marc/slck/issues"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"build",
|
||||
"toml",
|
||||
"pytest",
|
||||
"black",
|
||||
"pylint",
|
||||
"pytest-cov",
|
||||
"syrupy",
|
||||
"pytest~=8.0"
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [ "slack_status_cli",]
|
||||
[project.scripts]
|
||||
slck = "main:run"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
astroid==3.0.3
|
||||
# via pylint
|
||||
black==24.1.1
|
||||
# via slack-status-cli (pyproject.toml)
|
||||
build==1.0.3
|
||||
# via slack-status-cli (pyproject.toml)
|
||||
click==8.1.7
|
||||
# via black
|
||||
coverage[toml]==7.4.1
|
||||
# via pytest-cov
|
||||
dill==0.3.8
|
||||
# via pylint
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
isort==5.13.2
|
||||
# via pylint
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
mypy-extensions==1.0.0
|
||||
# via black
|
||||
packaging==23.2
|
||||
# via
|
||||
# black
|
||||
# build
|
||||
# pytest
|
||||
pathspec==0.12.1
|
||||
# via black
|
||||
platformdirs==4.2.0
|
||||
# via
|
||||
# black
|
||||
# pylint
|
||||
pluggy==1.4.0
|
||||
# via pytest
|
||||
pylint==3.0.3
|
||||
# via slack-status-cli (pyproject.toml)
|
||||
pyproject-hooks==1.0.0
|
||||
# via build
|
||||
pytest==7.4.4
|
||||
# via
|
||||
# pytest-cov
|
||||
# slack-status-cli (pyproject.toml)
|
||||
# syrupy
|
||||
pytest-cov==4.1.0
|
||||
# via slack-status-cli (pyproject.toml)
|
||||
syrupy==4.6.0
|
||||
# via slack-status-cli (pyproject.toml)
|
||||
toml==0.10.2
|
||||
# via slack-status-cli (pyproject.toml)
|
||||
tomlkit==0.12.3
|
||||
# via pylint
|
|
@ -1,14 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
PROJECT="slack-status-cli"
|
||||
|
||||
python -m pip install --upgrade setuptools
|
||||
python -m pip install pip~=23.0 pip-tools~=7.3 --no-cache
|
||||
|
||||
if [ ! -d "./$PROJECT.venv" ]; then
|
||||
python -m venv ./$PROJECT.venv
|
||||
fi
|
||||
|
||||
source ./$PROJECT.venv/bin/activate
|
||||
|
||||
pip-sync ./requirements.txt ./requirements_dev.txt
|
|
@ -1,25 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
PROD_DEP="requirements.txt"
|
||||
DEV_DEP="requirements_dev.txt"
|
||||
|
||||
echo "Locking production dependencies as $PROD_DEP"
|
||||
|
||||
python -m piptools compile \
|
||||
-o $PROD_DEP \
|
||||
--no-header \
|
||||
pyproject.toml
|
||||
|
||||
if [[ $? != 0 ]]; then
|
||||
echo "Failed to lock production dependencies."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Locking production dependencies as $DEV_DEP"
|
||||
|
||||
python -m piptools compile \
|
||||
-o $DEV_DEP \
|
||||
--no-header \
|
||||
--extra dev \
|
||||
--constraint $PROD_DEP \
|
||||
pyproject.toml
|
|
@ -1,21 +0,0 @@
|
|||
"""
|
||||
Fixes the version of pyproject.toml, for use before the package
|
||||
artifacts are built.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import toml
|
||||
|
||||
|
||||
def set_version(new_version: str):
|
||||
with open("./pyproject.toml", "r", encoding="utf8") as pyproject:
|
||||
project_config = toml.loads(pyproject.read())
|
||||
project_config["project"]["version"] = new_version
|
||||
|
||||
print("Bumped version to %s" % new_version)
|
||||
|
||||
with open("./pyproject.toml", "w", encoding="utf8") as pyproject:
|
||||
toml.dump(project_config, pyproject)
|
||||
|
||||
|
||||
set_version(sys.argv[1])
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.0"
|
|
@ -1,255 +0,0 @@
|
|||
"""
|
||||
CLI Slack Status Handling
|
||||
|
||||
Provides a shortcut to set Slack statuses from the command-line. Since statuses are often
|
||||
canned, this tool also facilitates setting up presets that can be quickly invoked.
|
||||
|
||||
With custom text:
|
||||
|
||||
SLACK_TOKEN=XXX slack-status-cli set --text <text> --icon <icon> --duration <duration_description>
|
||||
|
||||
With preset:
|
||||
|
||||
SLACK_TOKEN=XXX slack-status-cli set --preset <preset-name> --duration <duration_description>
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import typing
|
||||
import os
|
||||
import logging
|
||||
import argparse
|
||||
import datetime
|
||||
import collections
|
||||
import re
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
# Debug mode modifies the log level used for reporting. If truthy,
|
||||
# extra information is included in each run to diagnose common
|
||||
# issues.
|
||||
DEBUG = bool(os.environ.get("DEBUG", False))
|
||||
|
||||
# Logger setup
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.propagate = False
|
||||
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
||||
log_handler = logging.StreamHandler()
|
||||
log_handler.setLevel(level=logging.DEBUG if DEBUG else logging.INFO)
|
||||
log_handler.setFormatter(logging.Formatter(fmt="%(message)s"))
|
||||
logger.addHandler(log_handler)
|
||||
|
||||
ParsedUserInput = collections.namedtuple(
|
||||
"ParsedUserInput", ["text", "icon", "duration", "preset"]
|
||||
)
|
||||
|
||||
StatusPreset = collections.namedtuple("StatusPreset", ["text", "icon"])
|
||||
Defaults = collections.namedtuple(
|
||||
"Defaults",
|
||||
[("icon"), ("duration")],
|
||||
defaults=[None, None],
|
||||
)
|
||||
Configuration = collections.namedtuple(
|
||||
"Configuration",
|
||||
[
|
||||
("presets"),
|
||||
("defaults"),
|
||||
],
|
||||
defaults=[{}, Defaults()],
|
||||
)
|
||||
|
||||
|
||||
def update_status(
|
||||
token: str,
|
||||
status: str,
|
||||
emoticon: typing.Optional[str] = None,
|
||||
expiration: typing.Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Sets the Slack status of the given user to <status>, optionally with <emoticon> if provided.
|
||||
If an expiration is provided, the status is set to expire after this time.
|
||||
"""
|
||||
payload = {
|
||||
"profile": {
|
||||
"status_text": status,
|
||||
"status_emoji": emoticon or "",
|
||||
"status_expiration": expiration or 0,
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
"https://slack.com/api/users.profile.set",
|
||||
urllib.parse.urlencode(payload).encode(),
|
||||
method="POST",
|
||||
)
|
||||
for header_key, header_value in headers.items():
|
||||
request.add_header(header_key, header_value)
|
||||
|
||||
response = urllib.request.urlopen(request)
|
||||
response_status = response.status
|
||||
response_data = response.read()
|
||||
|
||||
logger.debug("API request: %s", str(payload))
|
||||
logger.debug("API response: %s", str(response_data))
|
||||
|
||||
if response_status != 200:
|
||||
raise Exception("Failed to set status due to an API error.")
|
||||
|
||||
response_data = json.loads(response_data)
|
||||
|
||||
if not response_data["ok"]:
|
||||
raise Exception("Failed to set status due to an API error.")
|
||||
|
||||
|
||||
def parse_input(known_presets: typing.List[str]) -> ParsedUserInput:
|
||||
"""
|
||||
Handles command-line argument parsing and help text display.
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
set_parser = subparsers.add_parser("set", help="Set your Slack status")
|
||||
set_parser.add_argument("--text", type=str, help="Status text")
|
||||
set_parser.add_argument(
|
||||
"--icon",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Status icon (as defined by your workspace) in :icon: format",
|
||||
)
|
||||
set_parser.add_argument(
|
||||
"--duration",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Status duration, formatted as AdBhCm (each segment is optional)",
|
||||
)
|
||||
set_parser.add_argument(
|
||||
"--preset", type=str, default=None, choices=known_presets, help="Preset to use"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return ParsedUserInput(
|
||||
text=args.text, icon=args.icon, duration=args.duration, preset=args.preset
|
||||
)
|
||||
|
||||
|
||||
def get_expiration(duration_description: typing.Optional[str] = None) -> int:
|
||||
"""
|
||||
Gets an expiration timestamp based on a duration description string of the
|
||||
format <int>d<int>h<int>m.
|
||||
"""
|
||||
|
||||
if not duration_description:
|
||||
return 0
|
||||
|
||||
DURATION_PATTERN = (
|
||||
r"^((?P<days>[0-9]+)d)?((?P<hours>[0-9]+)h)?((?P<minutes>[0-9]+)m)?$"
|
||||
)
|
||||
|
||||
duration_input = re.match(DURATION_PATTERN, duration_description)
|
||||
duration_parts = duration_input.groupdict()
|
||||
|
||||
duration = datetime.timedelta(
|
||||
days=int(duration_parts.get("days") or 0),
|
||||
minutes=int(duration_parts.get("minutes") or 0),
|
||||
hours=int(duration_parts.get("hours") or 0),
|
||||
)
|
||||
|
||||
return (datetime.datetime.now() + duration).timestamp()
|
||||
|
||||
|
||||
def load_configuration() -> Configuration:
|
||||
"""
|
||||
Loads from configuration file if present.
|
||||
"""
|
||||
|
||||
configuration_path = pathlib.Path.home().joinpath(".config", "slack-status-cli")
|
||||
|
||||
if not configuration_path.exists():
|
||||
return Configuration()
|
||||
|
||||
with open(configuration_path, "r", encoding="utf-8") as config_file:
|
||||
config = config_file.read()
|
||||
|
||||
try:
|
||||
parsed_config = json.loads(config)
|
||||
|
||||
logger.debug("Loaded configuration: %s", parsed_config)
|
||||
|
||||
preset_config = parsed_config.get("presets", {})
|
||||
defaults_config = parsed_config.get("defaults", {})
|
||||
|
||||
presets = {
|
||||
preset_key: StatusPreset(
|
||||
text=preset_value["text"], icon=preset_value.get("icon")
|
||||
)
|
||||
for preset_key, preset_value in preset_config.items()
|
||||
}
|
||||
|
||||
defaults = Defaults(
|
||||
icon=defaults_config.get("icon"), duration=defaults_config.get("duration")
|
||||
)
|
||||
|
||||
return Configuration(presets=presets, defaults=defaults)
|
||||
|
||||
except Exception:
|
||||
logger.warning("Invalid configuration found at %s", str(configuration_path))
|
||||
return Configuration()
|
||||
|
||||
|
||||
def run():
|
||||
try:
|
||||
configuration = load_configuration()
|
||||
|
||||
args = parse_input(configuration.presets.keys())
|
||||
|
||||
if args.preset and not args.preset in configuration.presets:
|
||||
raise Exception("Unknown preset %s" % args.preset)
|
||||
|
||||
token = os.environ.get("SLACK_TOKEN")
|
||||
|
||||
if not token:
|
||||
raise Exception("Slack token not provided.")
|
||||
|
||||
status_text = args.text
|
||||
status_icon = args.icon
|
||||
status_expiration = get_expiration(
|
||||
args.duration or configuration.defaults.duration
|
||||
)
|
||||
|
||||
if args.preset:
|
||||
preset = configuration.presets[args.preset]
|
||||
status_text = preset.text
|
||||
status_icon = preset.icon
|
||||
|
||||
update_status(
|
||||
token,
|
||||
status_text,
|
||||
status_icon or configuration.defaults.icon,
|
||||
status_expiration,
|
||||
)
|
||||
|
||||
new_status = (
|
||||
"%s %s" % (status_icon, status_text) if status_icon else status_text
|
||||
)
|
||||
new_expiry = (
|
||||
"(expires %s)"
|
||||
% datetime.datetime.fromtimestamp(status_expiration).strftime(
|
||||
"%A, %B %d, %H:%M"
|
||||
)
|
||||
if status_expiration
|
||||
else "(no expiration)"
|
||||
)
|
||||
logger.info("✨ Status set to '%s' %s", new_status, new_expiry)
|
||||
except Exception as e:
|
||||
logger.error("🔥 Could not set status: %s", str(e))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
173
test.py
Normal file
173
test.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from main import cli, get_configuration
|
||||
|
||||
|
||||
@pytest.fixture(name="with_sample_config")
|
||||
def fixture_with_sample_config(tmp_path):
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(
|
||||
"""
|
||||
token: abc
|
||||
presets:
|
||||
test:
|
||||
text: abc
|
||||
emoji: ":tada:"
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_get_configuration_raises_if_noexist():
|
||||
with pytest.raises(RuntimeError):
|
||||
get_configuration("not/a/path.yml")
|
||||
|
||||
|
||||
def test_get_configuration_returns_configuration_obj(tmp_path):
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(
|
||||
"""
|
||||
token: abc
|
||||
presets:
|
||||
test:
|
||||
text: abc
|
||||
emoji: ":tada:"
|
||||
"""
|
||||
)
|
||||
|
||||
conf = get_configuration(config_path)
|
||||
|
||||
assert conf.token == "abc"
|
||||
assert "test" in conf.presets
|
||||
assert conf.presets["test"].text == "abc"
|
||||
assert conf.presets["test"].emoji == ":tada:"
|
||||
|
||||
|
||||
def test_cli_requires_text_or_preset_input(tmp_path, with_sample_config):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--config", tmp_path / "config.yml"])
|
||||
|
||||
assert isinstance(result.exception, RuntimeError)
|
||||
assert (
|
||||
str(result.exception)
|
||||
== "Must specify either status text via --text/-t or a preset via --preset/-p."
|
||||
)
|
||||
|
||||
|
||||
def test_cli_overrides_preset_with_text_input(with_sample_config, tmp_path):
|
||||
runner = CliRunner()
|
||||
|
||||
mock_client = Mock()
|
||||
mock_client.users_profile_set = Mock()
|
||||
with patch(
|
||||
"main.get_client", autospec=True, return_value=mock_client
|
||||
):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--text",
|
||||
"testtext",
|
||||
"--preset",
|
||||
"test",
|
||||
"--config",
|
||||
tmp_path / "config.yml",
|
||||
],
|
||||
)
|
||||
|
||||
call_args = mock_client.users_profile_set.call_args
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert call_args.kwargs["profile"]["status_text"] == "testtext"
|
||||
assert call_args.kwargs["profile"]["status_emoji"] == ":tada:"
|
||||
|
||||
|
||||
def test_cli_overrides_preset_with_emoji_input(with_sample_config, tmp_path):
|
||||
runner = CliRunner()
|
||||
|
||||
mock_client = Mock()
|
||||
mock_client.users_profile_set = Mock()
|
||||
with patch(
|
||||
"main.get_client", autospec=True, return_value=mock_client
|
||||
):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--emoji",
|
||||
":skull:",
|
||||
"--preset",
|
||||
"test",
|
||||
"--config",
|
||||
tmp_path / "config.yml",
|
||||
],
|
||||
)
|
||||
|
||||
call_args = mock_client.users_profile_set.call_args
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert call_args.kwargs["profile"]["status_text"] == "abc"
|
||||
assert call_args.kwargs["profile"]["status_emoji"] == ":skull:"
|
||||
|
||||
|
||||
def test_cli_raises_if_noexist_preset(tmp_path, with_sample_config):
|
||||
runner = CliRunner()
|
||||
|
||||
mock_client = Mock()
|
||||
mock_client.users_profile_set = Mock()
|
||||
with patch(
|
||||
"main.get_client", autospec=True, return_value=mock_client
|
||||
):
|
||||
result = runner.invoke(
|
||||
cli, ["--preset", "not-a-preset", "--config", tmp_path / "config.yml"]
|
||||
)
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert str(result.exception) == "Unknown preset: not-a-preset"
|
||||
|
||||
|
||||
def test_cli_sends_request_to_slack(tmp_path, with_sample_config):
|
||||
runner = CliRunner()
|
||||
|
||||
mock_client = Mock()
|
||||
mock_client.users_profile_set = Mock()
|
||||
with patch(
|
||||
"main.get_client", autospec=True, return_value=mock_client
|
||||
):
|
||||
runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--emoji",
|
||||
":skull:",
|
||||
"--preset",
|
||||
"test",
|
||||
"--config",
|
||||
tmp_path / "config.yml",
|
||||
],
|
||||
)
|
||||
|
||||
mock_client.users_profile_set.assert_called()
|
||||
|
||||
|
||||
def test_cli_raises_if_api_error(tmp_path, with_sample_config):
|
||||
runner = CliRunner()
|
||||
|
||||
mock_client = Mock()
|
||||
mock_client.users_profile_set = Mock(return_value={"ok": False})
|
||||
with patch(
|
||||
"main.get_client", autospec=True, return_value=mock_client
|
||||
):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"--emoji",
|
||||
":skull:",
|
||||
"--preset",
|
||||
"test",
|
||||
"--config",
|
||||
tmp_path / "config.yml",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert str(result.exception) == "Failed to set status!"
|
|
@ -1,8 +0,0 @@
|
|||
# serializer version: 1
|
||||
# name: test_sends_request_to_slack_api_on_success
|
||||
dict({
|
||||
'profile': list([
|
||||
"{'status_text': 'test', 'status_emoji': '', 'status_expiration': 0}",
|
||||
]),
|
||||
})
|
||||
# ---
|
|
@ -1,6 +0,0 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def slack_api_token():
|
||||
return "mock-slack-token"
|
|
@ -1,59 +0,0 @@
|
|||
import unittest.mock
|
||||
import urllib.parse
|
||||
import os
|
||||
import sys
|
||||
import typing
|
||||
|
||||
import slack_status_cli.main
|
||||
|
||||
|
||||
class MockResponse(typing.NamedTuple):
|
||||
"""
|
||||
Stand-in for http.client.HTTPResponse.
|
||||
"""
|
||||
|
||||
status: int
|
||||
response_text: str
|
||||
|
||||
def read(self):
|
||||
return self.response_text
|
||||
|
||||
|
||||
def test_errors_if_no_slack_token_provided(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["slack-status-cli", "set", "--text", "test"])
|
||||
|
||||
with unittest.mock.patch("sys.exit", autospec=True) as mock_exit:
|
||||
slack_status_cli.main.run()
|
||||
|
||||
mock_exit.assert_called_with(1)
|
||||
|
||||
|
||||
def test_sends_request_to_slack_api_on_success(
|
||||
slack_api_token, monkeypatch, snapshot, tmp_path
|
||||
):
|
||||
env = os.environ.copy()
|
||||
env["SLACK_TOKEN"] = slack_api_token
|
||||
|
||||
mock_response = MockResponse(status=200, response_text='{ "ok": true }')
|
||||
|
||||
monkeypatch.setenv("SLACK_TOKEN", slack_api_token)
|
||||
monkeypatch.setattr(sys, "argv", ["slack-status-cli", "set", "--text", "test"])
|
||||
|
||||
with unittest.mock.patch(
|
||||
"urllib.request.urlopen",
|
||||
autospec=True,
|
||||
return_value=mock_response,
|
||||
) as mock_request, unittest.mock.patch(
|
||||
"pathlib.Path.home", autospec=True, return_value=tmp_path
|
||||
):
|
||||
slack_status_cli.main.run()
|
||||
|
||||
request = mock_request.call_args_list[0][0][0]
|
||||
|
||||
assert request.get_full_url() == "https://slack.com/api/users.profile.set"
|
||||
assert request.get_method() == "POST"
|
||||
assert request.get_header("Authorization") == "Bearer %s" % slack_api_token
|
||||
|
||||
request_body = urllib.parse.parse_qs(request.data.decode())
|
||||
|
||||
assert request_body == snapshot
|
Loading…
Reference in a new issue