feat: auth backend (#111)
* refactor(types): extract inline anonymous types into named classes
Problem: several functions used inline `{...}` table types in their
`@param` and `@return` annotations, making them hard to read and
impossible to reference from other modules.
Solution: extract each into a named `---@class`: `pending.Metadata`,
`pending.TaskFields`, `pending.CompletionItem`, `pending.SystemResult`,
and `pending.OAuthClientOpts`.
* refactor(sync): extract shared utilities into `sync/util.lua`
Problem: sync epilogue code (`s:save()`, `_recompute_counts()`,
`buffer.render()`) and `fmt_counts` were duplicated across `gcal.lua`
and `gtasks.lua`. The concurrency guard lived in `oauth.lua`, coupling
non-OAuth backends to the OAuth module.
Solution: create `sync/util.lua` with `async`, `system`, `with_guard`,
`finish`, and `fmt_counts`. Delegate from `oauth.lua` and replace
duplicated code in both backends. Add per-backend `auth()` and
`auth_complete()` methods to `gcal.lua` and `gtasks.lua`.
* feat(sync): auto-discover backends, per-backend auth, S3 backend
Problem: sync backends were hardcoded in `SYNC_BACKENDS` list in
`init.lua`, auth routed directly through `oauth.google_client`, and
adding a non-OAuth backend required editing multiple files.
Solution: replace hardcoded list with `discover_backends()` that globs
`lua/pending/sync/*.lua` at runtime. Rewrite `M.auth()` to dispatch
to per-backend `auth()` methods with `vim.ui.select` fallback. Add
`lua/pending/sync/s3.lua` with push/pull/sync via AWS CLI, per-task
merge by `_s3_sync_id` (UUID), and `pending.S3Config` type.
This commit is contained in:
parent
ac02526cf1
commit
fe4c1d0e31
13 changed files with 1173 additions and 107 deletions
101
spec/sync_util_spec.lua
Normal file
101
spec/sync_util_spec.lua
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local util = require('pending.sync.util')
|
||||
|
||||
describe('sync util', function()
|
||||
before_each(function()
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('fmt_counts', function()
|
||||
it('returns nothing to do for empty counts', function()
|
||||
assert.equals('nothing to do', util.fmt_counts({}))
|
||||
end)
|
||||
|
||||
it('returns nothing to do when all zero', function()
|
||||
assert.equals('nothing to do', util.fmt_counts({ { 0, 'added' }, { 0, 'failed' } }))
|
||||
end)
|
||||
|
||||
it('formats single non-zero count', function()
|
||||
assert.equals('3 added', util.fmt_counts({ { 3, 'added' }, { 0, 'failed' } }))
|
||||
end)
|
||||
|
||||
it('joins multiple non-zero counts with pipe', function()
|
||||
local result = util.fmt_counts({ { 2, 'added' }, { 1, 'updated' }, { 0, 'failed' } })
|
||||
assert.equals('2 added | 1 updated', result)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('with_guard', function()
|
||||
it('prevents concurrent calls', function()
|
||||
local inner_called = false
|
||||
local blocked = false
|
||||
|
||||
local msgs = {}
|
||||
local orig = vim.notify
|
||||
vim.notify = function(m, level)
|
||||
if level == vim.log.levels.WARN then
|
||||
table.insert(msgs, m)
|
||||
end
|
||||
end
|
||||
|
||||
util.with_guard('test', function()
|
||||
inner_called = true
|
||||
util.with_guard('test2', function()
|
||||
blocked = true
|
||||
end)
|
||||
end)
|
||||
|
||||
vim.notify = orig
|
||||
assert.is_true(inner_called)
|
||||
assert.is_false(blocked)
|
||||
assert.equals(1, #msgs)
|
||||
assert.truthy(msgs[1]:find('Sync already in progress'))
|
||||
end)
|
||||
|
||||
it('clears guard after error', function()
|
||||
pcall(util.with_guard, 'err-test', function()
|
||||
error('boom')
|
||||
end)
|
||||
|
||||
assert.is_false(util.sync_in_flight())
|
||||
end)
|
||||
|
||||
it('clears guard after success', function()
|
||||
util.with_guard('ok-test', function() end)
|
||||
assert.is_false(util.sync_in_flight())
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('finish', function()
|
||||
it('calls save and recompute', function()
|
||||
local helpers = require('spec.helpers')
|
||||
local store_mod = require('pending.store')
|
||||
local tmpdir = helpers.tmpdir()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
package.loaded['pending'] = nil
|
||||
local pending = require('pending')
|
||||
|
||||
local s = store_mod.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
s:add({ description = 'Test', status = 'pending', category = 'Work', priority = 0 })
|
||||
|
||||
util.finish(s)
|
||||
|
||||
local reloaded = store_mod.new(tmpdir .. '/tasks.json')
|
||||
reloaded:load()
|
||||
assert.equals(1, #reloaded:tasks())
|
||||
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue