require('spec.helpers') local config = require('pending.config') local oauth = require('pending.sync.oauth') describe('oauth', function() local tmpdir before_each(function() tmpdir = vim.fn.tempname() vim.fn.mkdir(tmpdir, 'p') vim.g.pending = { data_path = tmpdir .. '/tasks.json' } config.reset() end) after_each(function() vim.fn.delete(tmpdir, 'rf') vim.g.pending = nil config.reset() end) describe('url_encode', function() it('leaves alphanumerics unchanged', function() assert.equals('hello123', oauth.url_encode('hello123')) end) it('encodes spaces', function() assert.equals('hello%20world', oauth.url_encode('hello world')) end) it('encodes special characters', function() assert.equals('a%3Db%26c', oauth.url_encode('a=b&c')) end) it('preserves hyphens, dots, underscores, tildes', function() assert.equals('a-b.c_d~e', oauth.url_encode('a-b.c_d~e')) end) end) describe('load_json_file', function() it('returns nil for missing file', function() assert.is_nil(oauth.load_json_file(tmpdir .. '/nonexistent.json')) end) it('returns nil for empty file', function() local path = tmpdir .. '/empty.json' local f = io.open(path, 'w') f:write('') f:close() assert.is_nil(oauth.load_json_file(path)) end) it('returns nil for invalid JSON', function() local path = tmpdir .. '/bad.json' local f = io.open(path, 'w') f:write('not json') f:close() assert.is_nil(oauth.load_json_file(path)) end) it('parses valid JSON', function() local path = tmpdir .. '/good.json' local f = io.open(path, 'w') f:write('{"key":"value"}') f:close() local data = oauth.load_json_file(path) assert.equals('value', data.key) end) end) describe('save_json_file', function() it('creates parent directories', function() local path = tmpdir .. '/sub/dir/file.json' local ok = oauth.save_json_file(path, { test = true }) assert.is_true(ok) local data = oauth.load_json_file(path) assert.is_true(data.test) end) it('sets restrictive permissions', function() local path = tmpdir .. '/secret.json' oauth.save_json_file(path, { x = 1 }) local perms = vim.fn.getfperm(path) assert.equals('rw-------', perms) end) end) describe('resolve_credentials', function() it('uses config fields when set', function() config.reset() vim.g.pending = { data_path = tmpdir .. '/tasks.json', sync = { gtasks = { client_id = 'config-id', client_secret = 'config-secret', }, }, } local c = oauth.new({ name = 'gtasks', scope = 'x', port = 0, config_key = 'gtasks' }) local creds = c:resolve_credentials() assert.equals('config-id', creds.client_id) assert.equals('config-secret', creds.client_secret) end) it('uses credentials file when config fields absent', function() local cred_path = tmpdir .. '/creds.json' oauth.save_json_file(cred_path, { client_id = 'file-id', client_secret = 'file-secret', }) config.reset() vim.g.pending = { data_path = tmpdir .. '/tasks.json', sync = { gtasks = { credentials_path = cred_path } }, } local c = oauth.new({ name = 'gtasks', scope = 'x', port = 0, config_key = 'gtasks' }) local creds = c:resolve_credentials() assert.equals('file-id', creds.client_id) assert.equals('file-secret', creds.client_secret) end) it('unwraps installed wrapper format', function() local cred_path = tmpdir .. '/wrapped.json' oauth.save_json_file(cred_path, { installed = { client_id = 'wrapped-id', client_secret = 'wrapped-secret', }, }) config.reset() vim.g.pending = { data_path = tmpdir .. '/tasks.json', sync = { gcal = { credentials_path = cred_path } }, } local c = oauth.new({ name = 'gcal', scope = 'x', port = 0, config_key = 'gcal' }) local creds = c:resolve_credentials() assert.equals('wrapped-id', creds.client_id) assert.equals('wrapped-secret', creds.client_secret) end) it('falls back to bundled credentials', function() config.reset() vim.g.pending = { data_path = tmpdir .. '/tasks.json' } local orig_load = oauth.load_json_file oauth.load_json_file = function() return nil end local c = oauth.new({ name = 'gtasks', scope = 'x', port = 0, config_key = 'gtasks' }) local creds = c:resolve_credentials() oauth.load_json_file = orig_load assert.equals(oauth._BUNDLED_CLIENT_ID, creds.client_id) assert.equals(oauth._BUNDLED_CLIENT_SECRET, creds.client_secret) end) it('prefers config fields over credentials file', function() local cred_path = tmpdir .. '/creds2.json' oauth.save_json_file(cred_path, { client_id = 'file-id', client_secret = 'file-secret', }) config.reset() vim.g.pending = { data_path = tmpdir .. '/tasks.json', sync = { gtasks = { credentials_path = cred_path, client_id = 'config-id', client_secret = 'config-secret', }, }, } local c = oauth.new({ name = 'gtasks', scope = 'x', port = 0, config_key = 'gtasks' }) local creds = c:resolve_credentials() assert.equals('config-id', creds.client_id) assert.equals('config-secret', creds.client_secret) end) end) describe('token_path', function() it('includes backend name', function() local c = oauth.new({ name = 'gtasks', scope = 'x', port = 0, config_key = 'gtasks' }) assert.truthy(c:token_path():match('gtasks_tokens%.json$')) end) it('differs between backends', function() local g = oauth.new({ name = 'gcal', scope = 'x', port = 0, config_key = 'gcal' }) local t = oauth.new({ name = 'gtasks', scope = 'x', port = 0, config_key = 'gtasks' }) assert.not_equals(g:token_path(), t:token_path()) end) end) describe('load_tokens / save_tokens', function() it('round-trips tokens', function() local c = oauth.new({ name = 'test', scope = 'x', port = 0, config_key = 'gtasks' }) local path = c:token_path() local dir = vim.fn.fnamemodify(path, ':h') vim.fn.mkdir(dir, 'p') local tokens = { access_token = 'at', refresh_token = 'rt', expires_in = 3600, obtained_at = 1000, } c:save_tokens(tokens) local loaded = c:load_tokens() assert.equals('at', loaded.access_token) assert.equals('rt', loaded.refresh_token) vim.fn.delete(dir, 'rf') end) end) describe('auth_headers', function() it('includes bearer token', function() local headers = oauth.auth_headers('mytoken') assert.equals('Authorization: Bearer mytoken', headers[1]) assert.equals('Content-Type: application/json', headers[2]) end) end) describe('new', function() it('creates client with correct fields', function() local c = oauth.new({ name = 'test', scope = 'https://example.com', port = 12345, config_key = 'test', }) assert.equals('test', c.name) assert.equals('https://example.com', c.scope) assert.equals(12345, c.port) assert.equals('test', c.config_key) end) end) end)