feat(sync): backend interface + CLI refactor (#42)
* refactor(sync): extract backend interface, adapt gcal module
Problem: :Pending sync hardcodes Google Calendar — M.sync() does
pcall(require, 'pending.sync.gcal') and calls gcal.sync() directly.
The config has a flat gcal field. This prevents adding new sync backends
without modifying init.lua.
Solution: Define a backend interface contract (name, auth, sync, health
fields), refactor :Pending sync to dispatch via require('pending.sync.'
.. backend_name), add sync table to config with legacy gcal migration,
rename gcal.authorize to gcal.auth, add gcal.health for checkhealth,
and add tab completion for backend names and actions.
* docs(sync): update vimdoc for backend interface
Problem: Vimdoc documents :Pending sync as a bare command that pushes
to Google Calendar, with no mention of backends or the sync table config.
Solution: Update :Pending sync section to show {backend} [{action}]
syntax with examples, add SYNC BACKENDS section documenting the interface
contract, update config example to use sync.gcal, document legacy gcal
migration, and update health check description.
* test(sync): add backend dispatch tests
Problem: No test coverage for sync dispatch logic, config migration,
or gcal module interface conformance.
Solution: Add spec/sync_spec.lua with tests for: bare sync errors,
empty backend errors, unknown backend errors, unknown action errors,
default-to-sync routing, explicit sync/auth routing, legacy gcal config
migration, explicit sync.gcal precedence, and gcal module interface
fields (name, auth, sync, health).
This commit is contained in:
parent
85cf0d42ed
commit
306e11aee6
7 changed files with 327 additions and 36 deletions
|
|
@ -47,7 +47,7 @@ REQUIREMENTS *pending-requirements*
|
||||||
|
|
||||||
- Neovim 0.10+
|
- Neovim 0.10+
|
||||||
- No external dependencies for local use
|
- No external dependencies for local use
|
||||||
- `curl` and `openssl` are required for Google Calendar sync
|
- `curl` and `openssl` are required for the `gcal` sync backend
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
INSTALL *pending-install*
|
INSTALL *pending-install*
|
||||||
|
|
@ -250,10 +250,23 @@ COMMANDS *pending-commands*
|
||||||
Open the list with |:copen| to navigate to each task's category.
|
Open the list with |:copen| to navigate to each task's category.
|
||||||
|
|
||||||
*:Pending-sync*
|
*:Pending-sync*
|
||||||
:Pending sync
|
:Pending sync {backend} [{action}]
|
||||||
Push pending tasks that have a due date to Google Calendar as all-day
|
Run a sync action against a named backend. {backend} is required — bare
|
||||||
events. Requires |pending-gcal| to be configured. See |pending-gcal| for
|
`:Pending sync` prints a usage message. {action} defaults to `sync`
|
||||||
full details on what gets created, updated, and deleted.
|
when omitted. Each backend lives at `lua/pending/sync/<name>.lua`.
|
||||||
|
|
||||||
|
Examples: >vim
|
||||||
|
:Pending sync gcal " runs gcal.sync()
|
||||||
|
:Pending sync gcal auth " runs gcal.auth()
|
||||||
|
:Pending sync gcal sync " explicit sync (same as bare)
|
||||||
|
<
|
||||||
|
|
||||||
|
Tab completion after `:Pending sync ` lists discovered backends.
|
||||||
|
Tab completion after `:Pending sync gcal ` lists available actions.
|
||||||
|
|
||||||
|
Built-in backends: ~
|
||||||
|
|
||||||
|
`gcal` Google Calendar one-way push. See |pending-gcal|.
|
||||||
|
|
||||||
*:Pending-undo*
|
*:Pending-undo*
|
||||||
:Pending undo
|
:Pending undo
|
||||||
|
|
@ -440,10 +453,12 @@ loads: >lua
|
||||||
next_task = ']t',
|
next_task = ']t',
|
||||||
prev_task = '[t',
|
prev_task = '[t',
|
||||||
},
|
},
|
||||||
|
sync = {
|
||||||
gcal = {
|
gcal = {
|
||||||
calendar = 'Tasks',
|
calendar = 'Tasks',
|
||||||
credentials_path = '/path/to/client_secret.json',
|
credentials_path = '/path/to/client_secret.json',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
||||||
|
|
@ -508,10 +523,16 @@ Fields: ~
|
||||||
vim.g.pending = { debug = true }
|
vim.g.pending = { debug = true }
|
||||||
<
|
<
|
||||||
|
|
||||||
|
{sync} (table, default: {}) *pending.SyncConfig*
|
||||||
|
Sync backend configuration. Each key is a backend
|
||||||
|
name and the value is the backend-specific config
|
||||||
|
table. Currently only `gcal` is built-in.
|
||||||
|
|
||||||
{gcal} (table, default: nil)
|
{gcal} (table, default: nil)
|
||||||
Google Calendar sync configuration. See
|
Legacy shorthand for `sync.gcal`. If `gcal` is set
|
||||||
|pending.GcalConfig|. Omit this field entirely to
|
but `sync.gcal` is not, the value is migrated
|
||||||
disable Google Calendar sync.
|
automatically. New configs should use `sync.gcal`
|
||||||
|
instead. See |pending.GcalConfig|.
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
LUA API *pending-api*
|
LUA API *pending-api*
|
||||||
|
|
@ -632,13 +653,18 @@ not pulled back into pending.nvim.
|
||||||
|
|
||||||
Configuration: >lua
|
Configuration: >lua
|
||||||
vim.g.pending = {
|
vim.g.pending = {
|
||||||
|
sync = {
|
||||||
gcal = {
|
gcal = {
|
||||||
calendar = 'Tasks',
|
calendar = 'Tasks',
|
||||||
credentials_path = '/path/to/client_secret.json',
|
credentials_path = '/path/to/client_secret.json',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
||||||
|
The legacy `gcal` top-level key is still accepted and migrated automatically.
|
||||||
|
New configurations should use `sync.gcal`.
|
||||||
|
|
||||||
*pending.GcalConfig*
|
*pending.GcalConfig*
|
||||||
Fields: ~
|
Fields: ~
|
||||||
{calendar} (string, default: 'Pendings')
|
{calendar} (string, default: 'Pendings')
|
||||||
|
|
@ -654,7 +680,7 @@ Fields: ~
|
||||||
that Google provides or as a bare credentials object.
|
that Google provides or as a bare credentials object.
|
||||||
|
|
||||||
OAuth flow: ~
|
OAuth flow: ~
|
||||||
On the first `:Pending sync` call the plugin detects that no refresh token
|
On the first `:Pending sync gcal` call the plugin detects that no refresh token
|
||||||
exists and opens the Google authorization URL in the browser using
|
exists and opens the Google authorization URL in the browser using
|
||||||
|vim.ui.open()|. A temporary local HTTP server listens on port 18392 for the
|
|vim.ui.open()|. A temporary local HTTP server listens on port 18392 for the
|
||||||
OAuth redirect. The PKCE (Proof Key for Code Exchange) flow is used —
|
OAuth redirect. The PKCE (Proof Key for Code Exchange) flow is used —
|
||||||
|
|
@ -664,7 +690,7 @@ authorization code is exchanged for tokens and the refresh token is stored at
|
||||||
use the stored refresh token and refresh the access token automatically when
|
use the stored refresh token and refresh the access token automatically when
|
||||||
it is about to expire.
|
it is about to expire.
|
||||||
|
|
||||||
`:Pending sync` behavior: ~
|
`:Pending sync gcal` behavior: ~
|
||||||
For each task in the store:
|
For each task in the store:
|
||||||
- A pending task with a due date and no existing event: a new all-day event is
|
- A pending task with a due date and no existing event: a new all-day event is
|
||||||
created and the event ID is stored in the task's `_extra` table.
|
created and the event ID is stored in the task's `_extra` table.
|
||||||
|
|
@ -677,6 +703,30 @@ For each task in the store:
|
||||||
A summary notification is shown after sync: `created: N, updated: N,
|
A summary notification is shown after sync: `created: N, updated: N,
|
||||||
deleted: N`.
|
deleted: N`.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
SYNC BACKENDS *pending-sync-backend*
|
||||||
|
|
||||||
|
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Each
|
||||||
|
module returns a table conforming to the backend interface: >lua
|
||||||
|
|
||||||
|
---@class pending.SyncBackend
|
||||||
|
---@field name string
|
||||||
|
---@field auth fun(): nil
|
||||||
|
---@field sync fun(): nil
|
||||||
|
---@field health? fun(): nil
|
||||||
|
<
|
||||||
|
|
||||||
|
Required fields: ~
|
||||||
|
{name} Backend identifier (matches the filename).
|
||||||
|
{sync} Main sync action. Called by `:Pending sync <name>`.
|
||||||
|
{auth} Authorization flow. Called by `:Pending sync <name> auth`.
|
||||||
|
|
||||||
|
Optional fields: ~
|
||||||
|
{health} Called by `:checkhealth pending` to report backend-specific
|
||||||
|
diagnostics (e.g. checking for external tools).
|
||||||
|
|
||||||
|
Backend-specific configuration goes under `sync.<name>` in |pending-config|.
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
HIGHLIGHT GROUPS *pending-highlights*
|
HIGHLIGHT GROUPS *pending-highlights*
|
||||||
|
|
||||||
|
|
@ -728,8 +778,8 @@ Checks performed: ~
|
||||||
- Whether the data directory exists (warning if not yet created)
|
- Whether the data directory exists (warning if not yet created)
|
||||||
- Whether the data file exists and can be parsed; reports total task count
|
- Whether the data file exists and can be parsed; reports total task count
|
||||||
- Validates recurrence specs on stored tasks
|
- Validates recurrence specs on stored tasks
|
||||||
- Whether `curl` is available (required for Google Calendar sync)
|
- Discovers sync backends under `lua/pending/sync/` and runs each backend's
|
||||||
- Whether `openssl` is available (required for OAuth PKCE)
|
`health()` function if it exists (e.g. gcal checks for `curl` and `openssl`)
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
DATA FORMAT *pending-data*
|
DATA FORMAT *pending-data*
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
---@field calendar? string
|
---@field calendar? string
|
||||||
---@field credentials_path? string
|
---@field credentials_path? string
|
||||||
|
|
||||||
|
---@class pending.SyncConfig
|
||||||
|
---@field gcal? pending.GcalConfig
|
||||||
|
|
||||||
---@class pending.Keymaps
|
---@class pending.Keymaps
|
||||||
---@field close? string|false
|
---@field close? string|false
|
||||||
---@field toggle? string|false
|
---@field toggle? string|false
|
||||||
|
|
@ -32,6 +35,7 @@
|
||||||
---@field drawer_height? integer
|
---@field drawer_height? integer
|
||||||
---@field debug? boolean
|
---@field debug? boolean
|
||||||
---@field keymaps pending.Keymaps
|
---@field keymaps pending.Keymaps
|
||||||
|
---@field sync? pending.SyncConfig
|
||||||
---@field gcal? pending.GcalConfig
|
---@field gcal? pending.GcalConfig
|
||||||
|
|
||||||
---@class pending.config
|
---@class pending.config
|
||||||
|
|
@ -65,6 +69,7 @@ local defaults = {
|
||||||
next_task = ']t',
|
next_task = ']t',
|
||||||
prev_task = '[t',
|
prev_task = '[t',
|
||||||
},
|
},
|
||||||
|
sync = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
---@type pending.Config?
|
---@type pending.Config?
|
||||||
|
|
@ -77,6 +82,10 @@ function M.get()
|
||||||
end
|
end
|
||||||
local user = vim.g.pending or {}
|
local user = vim.g.pending or {}
|
||||||
_resolved = vim.tbl_deep_extend('force', defaults, user)
|
_resolved = vim.tbl_deep_extend('force', defaults, user)
|
||||||
|
if _resolved.gcal and not (_resolved.sync and _resolved.sync.gcal) then
|
||||||
|
_resolved.sync = _resolved.sync or {}
|
||||||
|
_resolved.sync.gcal = _resolved.gcal
|
||||||
|
end
|
||||||
return _resolved
|
return _resolved
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,18 @@ function M.check()
|
||||||
vim.health.info('No data file yet (will be created on first save)')
|
vim.health.info('No data file yet (will be created on first save)')
|
||||||
end
|
end
|
||||||
|
|
||||||
if vim.fn.executable('curl') == 1 then
|
local sync_paths = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true)
|
||||||
vim.health.ok('curl found (required for Google Calendar sync)')
|
if #sync_paths == 0 then
|
||||||
|
vim.health.info('No sync backends found')
|
||||||
else
|
else
|
||||||
vim.health.warn('curl not found (needed for Google Calendar sync)')
|
for _, path in ipairs(sync_paths) do
|
||||||
|
local name = vim.fn.fnamemodify(path, ':t:r')
|
||||||
|
local bok, backend = pcall(require, 'pending.sync.' .. name)
|
||||||
|
if bok and type(backend.health) == 'function' then
|
||||||
|
vim.health.start('pending.nvim: sync/' .. name)
|
||||||
|
backend.health()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if vim.fn.executable('openssl') == 1 then
|
|
||||||
vim.health.ok('openssl found (required for OAuth PKCE)')
|
|
||||||
else
|
|
||||||
vim.health.warn('openssl not found (needed for Google Calendar OAuth)')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -414,14 +414,25 @@ function M.add(text)
|
||||||
vim.notify('Pending added: ' .. description)
|
vim.notify('Pending added: ' .. description)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param backend_name string
|
||||||
|
---@param action? string
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.sync()
|
function M.sync(backend_name, action)
|
||||||
local ok, gcal = pcall(require, 'pending.sync.gcal')
|
if not backend_name or backend_name == '' then
|
||||||
if not ok then
|
vim.notify('Usage: :Pending sync <backend> [action]', vim.log.levels.ERROR)
|
||||||
vim.notify('Google Calendar sync module not available.', vim.log.levels.ERROR)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
gcal.sync()
|
action = (action and action ~= '') and action or 'sync'
|
||||||
|
local ok, backend = pcall(require, 'pending.sync.' .. backend_name)
|
||||||
|
if not ok then
|
||||||
|
vim.notify('Unknown sync backend: ' .. backend_name, vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if type(backend[action]) ~= 'function' then
|
||||||
|
vim.notify(backend_name .. " backend has no '" .. action .. "' action", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
backend[action]()
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param days? integer
|
---@param days? integer
|
||||||
|
|
@ -700,7 +711,8 @@ function M.command(args)
|
||||||
local id_str, edit_rest = rest:match('^(%S+)%s*(.*)')
|
local id_str, edit_rest = rest:match('^(%S+)%s*(.*)')
|
||||||
M.edit(id_str, edit_rest)
|
M.edit(id_str, edit_rest)
|
||||||
elseif cmd == 'sync' then
|
elseif cmd == 'sync' then
|
||||||
M.sync()
|
local backend, action = rest:match('^(%S+)%s*(.*)')
|
||||||
|
M.sync(backend, action)
|
||||||
elseif cmd == 'archive' then
|
elseif cmd == 'archive' then
|
||||||
local d = rest ~= '' and tonumber(rest) or nil
|
local d = rest ~= '' and tonumber(rest) or nil
|
||||||
M.archive(d)
|
M.archive(d)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ local store = require('pending.store')
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
M.name = 'gcal'
|
||||||
|
|
||||||
local BASE_URL = 'https://www.googleapis.com/calendar/v3'
|
local BASE_URL = 'https://www.googleapis.com/calendar/v3'
|
||||||
local TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
local TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
||||||
local AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
local AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||||
|
|
@ -22,7 +24,7 @@ local SCOPE = 'https://www.googleapis.com/auth/calendar'
|
||||||
---@return table<string, any>
|
---@return table<string, any>
|
||||||
local function gcal_config()
|
local function gcal_config()
|
||||||
local cfg = config.get()
|
local cfg = config.get()
|
||||||
return cfg.gcal or {}
|
return (cfg.sync and cfg.sync.gcal) or cfg.gcal or {}
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return string
|
---@return string
|
||||||
|
|
@ -199,7 +201,7 @@ local function get_access_token()
|
||||||
end
|
end
|
||||||
local tokens = load_tokens()
|
local tokens = load_tokens()
|
||||||
if not tokens or not tokens.refresh_token then
|
if not tokens or not tokens.refresh_token then
|
||||||
M.authorize()
|
M.auth()
|
||||||
tokens = load_tokens()
|
tokens = load_tokens()
|
||||||
if not tokens then
|
if not tokens then
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -218,7 +220,7 @@ local function get_access_token()
|
||||||
return tokens.access_token
|
return tokens.access_token
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.authorize()
|
function M.auth()
|
||||||
local creds = load_credentials()
|
local creds = load_credentials()
|
||||||
if not creds then
|
if not creds then
|
||||||
vim.notify(
|
vim.notify(
|
||||||
|
|
@ -514,4 +516,18 @@ function M.sync()
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
|
function M.health()
|
||||||
|
if vim.fn.executable('curl') == 1 then
|
||||||
|
vim.health.ok('curl found (required for gcal sync)')
|
||||||
|
else
|
||||||
|
vim.health.warn('curl not found (needed for gcal sync)')
|
||||||
|
end
|
||||||
|
if vim.fn.executable('openssl') == 1 then
|
||||||
|
vim.health.ok('openssl found (required for gcal OAuth PKCE)')
|
||||||
|
else
|
||||||
|
vim.health.warn('openssl not found (needed for gcal OAuth)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,34 @@ end, {
|
||||||
if cmd_line:match('^Pending%s+edit') then
|
if cmd_line:match('^Pending%s+edit') then
|
||||||
return complete_edit(arg_lead, cmd_line)
|
return complete_edit(arg_lead, cmd_line)
|
||||||
end
|
end
|
||||||
|
if cmd_line:match('^Pending%s+sync') then
|
||||||
|
local after_sync = cmd_line:match('^Pending%s+sync%s+(.*)')
|
||||||
|
if not after_sync then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local parts = {}
|
||||||
|
for part in after_sync:gmatch('%S+') do
|
||||||
|
table.insert(parts, part)
|
||||||
|
end
|
||||||
|
local trailing_space = after_sync:match('%s$')
|
||||||
|
if #parts == 0 or (#parts == 1 and not trailing_space) then
|
||||||
|
local backends = {}
|
||||||
|
local pattern = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true)
|
||||||
|
for _, path in ipairs(pattern) do
|
||||||
|
local name = vim.fn.fnamemodify(path, ':t:r')
|
||||||
|
table.insert(backends, name)
|
||||||
|
end
|
||||||
|
table.sort(backends)
|
||||||
|
return filter_candidates(arg_lead, backends)
|
||||||
|
end
|
||||||
|
if #parts == 1 and trailing_space then
|
||||||
|
return filter_candidates(arg_lead, { 'auth', 'sync' })
|
||||||
|
end
|
||||||
|
if #parts >= 2 and not trailing_space then
|
||||||
|
return filter_candidates(arg_lead, { 'auth', 'sync' })
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
return {}
|
return {}
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
174
spec/sync_spec.lua
Normal file
174
spec/sync_spec.lua
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
require('spec.helpers')
|
||||||
|
|
||||||
|
local config = require('pending.config')
|
||||||
|
local store = require('pending.store')
|
||||||
|
|
||||||
|
describe('sync', function()
|
||||||
|
local tmpdir
|
||||||
|
local pending
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
tmpdir = vim.fn.tempname()
|
||||||
|
vim.fn.mkdir(tmpdir, 'p')
|
||||||
|
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||||
|
config.reset()
|
||||||
|
store.unload()
|
||||||
|
package.loaded['pending'] = nil
|
||||||
|
pending = require('pending')
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
vim.fn.delete(tmpdir, 'rf')
|
||||||
|
vim.g.pending = nil
|
||||||
|
config.reset()
|
||||||
|
store.unload()
|
||||||
|
package.loaded['pending'] = nil
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('dispatch', function()
|
||||||
|
it('errors on bare :Pending sync with no backend', function()
|
||||||
|
local msg
|
||||||
|
local orig = vim.notify
|
||||||
|
vim.notify = function(m, level)
|
||||||
|
if level == vim.log.levels.ERROR then
|
||||||
|
msg = m
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pending.sync(nil)
|
||||||
|
vim.notify = orig
|
||||||
|
assert.are.equal('Usage: :Pending sync <backend> [action]', msg)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('errors on empty backend string', function()
|
||||||
|
local msg
|
||||||
|
local orig = vim.notify
|
||||||
|
vim.notify = function(m, level)
|
||||||
|
if level == vim.log.levels.ERROR then
|
||||||
|
msg = m
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pending.sync('')
|
||||||
|
vim.notify = orig
|
||||||
|
assert.are.equal('Usage: :Pending sync <backend> [action]', msg)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('errors on unknown backend', function()
|
||||||
|
local msg
|
||||||
|
local orig = vim.notify
|
||||||
|
vim.notify = function(m, level)
|
||||||
|
if level == vim.log.levels.ERROR then
|
||||||
|
msg = m
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pending.sync('notreal')
|
||||||
|
vim.notify = orig
|
||||||
|
assert.are.equal('Unknown sync backend: notreal', msg)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('errors on unknown action for valid backend', function()
|
||||||
|
local msg
|
||||||
|
local orig = vim.notify
|
||||||
|
vim.notify = function(m, level)
|
||||||
|
if level == vim.log.levels.ERROR then
|
||||||
|
msg = m
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pending.sync('gcal', 'notreal')
|
||||||
|
vim.notify = orig
|
||||||
|
assert.are.equal("gcal backend has no 'notreal' action", msg)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('defaults to sync action when action is omitted', function()
|
||||||
|
local called = false
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
local orig_sync = gcal.sync
|
||||||
|
gcal.sync = function()
|
||||||
|
called = true
|
||||||
|
end
|
||||||
|
pending.sync('gcal')
|
||||||
|
gcal.sync = orig_sync
|
||||||
|
assert.is_true(called)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('routes explicit sync action', function()
|
||||||
|
local called = false
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
local orig_sync = gcal.sync
|
||||||
|
gcal.sync = function()
|
||||||
|
called = true
|
||||||
|
end
|
||||||
|
pending.sync('gcal', 'sync')
|
||||||
|
gcal.sync = orig_sync
|
||||||
|
assert.is_true(called)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('routes auth action', function()
|
||||||
|
local called = false
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
local orig_auth = gcal.auth
|
||||||
|
gcal.auth = function()
|
||||||
|
called = true
|
||||||
|
end
|
||||||
|
pending.sync('gcal', 'auth')
|
||||||
|
gcal.auth = orig_auth
|
||||||
|
assert.is_true(called)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('config migration', function()
|
||||||
|
it('migrates legacy gcal to sync.gcal', function()
|
||||||
|
config.reset()
|
||||||
|
vim.g.pending = {
|
||||||
|
data_path = tmpdir .. '/tasks.json',
|
||||||
|
gcal = { calendar = 'MyCalendar' },
|
||||||
|
}
|
||||||
|
local cfg = config.get()
|
||||||
|
assert.is_not_nil(cfg.sync)
|
||||||
|
assert.is_not_nil(cfg.sync.gcal)
|
||||||
|
assert.are.equal('MyCalendar', cfg.sync.gcal.calendar)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not overwrite explicit sync.gcal with legacy gcal', function()
|
||||||
|
config.reset()
|
||||||
|
vim.g.pending = {
|
||||||
|
data_path = tmpdir .. '/tasks.json',
|
||||||
|
gcal = { calendar = 'Legacy' },
|
||||||
|
sync = { gcal = { calendar = 'Explicit' } },
|
||||||
|
}
|
||||||
|
local cfg = config.get()
|
||||||
|
assert.are.equal('Explicit', cfg.sync.gcal.calendar)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('works with sync.gcal and no legacy gcal', function()
|
||||||
|
config.reset()
|
||||||
|
vim.g.pending = {
|
||||||
|
data_path = tmpdir .. '/tasks.json',
|
||||||
|
sync = { gcal = { calendar = 'NewStyle' } },
|
||||||
|
}
|
||||||
|
local cfg = config.get()
|
||||||
|
assert.are.equal('NewStyle', cfg.sync.gcal.calendar)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('gcal module', function()
|
||||||
|
it('has name field', function()
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
assert.are.equal('gcal', gcal.name)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('has auth function', function()
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
assert.are.equal('function', type(gcal.auth))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('has sync function', function()
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
assert.are.equal('function', type(gcal.sync))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('has health function', function()
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
assert.are.equal('function', type(gcal.health))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue