Remove the Go reference server; keep only Rust
CI / Rust (fmt / clippy / test / build) (push) Successful in 1m41s
CI / HTTP/JSON reference + SQLite persistence (push) Successful in 1m16s
CI / Rust (fmt / clippy / test / build) (pull_request) Successful in 47s
CI / HTTP/JSON reference + SQLite persistence (pull_request) Successful in 1m34s
CI / Rust (fmt / clippy / test / build) (push) Successful in 1m41s
CI / HTTP/JSON reference + SQLite persistence (push) Successful in 1m16s
CI / Rust (fmt / clippy / test / build) (pull_request) Successful in 47s
CI / HTTP/JSON reference + SQLite persistence (pull_request) Successful in 1m34s
The Go implementation is preserved in the v1.0.0 tag. Its observable behaviour stays the API reference: responses recorded from the Go server live in tools/compatibility_expected.txt, and tools/compatibility.py now compares the Rust server against them (--update re-records after an intentional change). The Go SQLite schema is frozen in tests/fixtures/go_schema_v1.sql for the existing-database test. Move the web client from internal/httpapi/web to web/, drop the Go CI job and Go version pin, and update README, AGENTS.md and docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
16dd2a3b3c
commit
10272f3851
33 files changed
+890
-1438
No files matched your search
@@ -0,0 +1,769 @@
|
||||
{'edge': {'expired refresh': (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_refresh_token',
|
||||
'message': 'Refresh token is invalid or expired'}}),
|
||||
('DELETE', '/app.js'): (405,
|
||||
{'allow': 'GET, HEAD',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('DELETE', '/health'): (405,
|
||||
{'allow': 'GET, HEAD',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('DELETE', '/v1/auth/login'): (405,
|
||||
{'allow': 'GET, HEAD, POST',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('DELETE', '/v1/me'): (405,
|
||||
{'allow': 'GET, HEAD',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('DELETE', '/v1/sync'): (405,
|
||||
{'allow': 'GET, HEAD, POST',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('HEAD', '/app.js'): (200, {'content-type': 'text/javascript; charset=utf-8'}, b''),
|
||||
('HEAD', '/health'): (200, {'content-type': 'application/json; charset=utf-8'}, b''),
|
||||
('HEAD', '/v1/auth/login'): (404,
|
||||
{'content-type': 'application/json; charset=utf-8'},
|
||||
b''),
|
||||
('HEAD', '/v1/me'): (401, {'content-type': 'application/json; charset=utf-8'}, b''),
|
||||
('HEAD', '/v1/sync'): (404, {'content-type': 'application/json; charset=utf-8'}, b''),
|
||||
('PATCH', '/app.js'): (405,
|
||||
{'allow': 'GET, HEAD',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PATCH', '/health'): (405,
|
||||
{'allow': 'GET, HEAD',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PATCH', '/v1/auth/login'): (405,
|
||||
{'allow': 'GET, HEAD, POST',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PATCH', '/v1/me'): (405,
|
||||
{'allow': 'GET, HEAD', 'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PATCH', '/v1/sync'): (405,
|
||||
{'allow': 'GET, HEAD, POST',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('POST', '/app.js'): (405,
|
||||
{'allow': 'GET, HEAD', 'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('POST', '/health'): (405,
|
||||
{'allow': 'GET, HEAD', 'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('POST', '/v1/auth/login'): (400,
|
||||
{'content-type': 'application/json; charset=utf-8'},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('POST', '/v1/me'): (405,
|
||||
{'allow': 'GET, HEAD', 'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('POST', '/v1/sync'): (401,
|
||||
{'content-type': 'application/json; charset=utf-8'},
|
||||
{'error': {'code': 'authorization_required',
|
||||
'message': 'Authorization Bearer token is required'}}),
|
||||
('PUT', '/app.js'): (405,
|
||||
{'allow': 'GET, HEAD', 'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PUT', '/health'): (405,
|
||||
{'allow': 'GET, HEAD', 'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PUT', '/v1/auth/login'): (405,
|
||||
{'allow': 'GET, HEAD, POST',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PUT', '/v1/me'): (405,
|
||||
{'allow': 'GET, HEAD', 'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('PUT', '/v1/sync'): (405,
|
||||
{'allow': 'GET, HEAD, POST',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'Method Not Allowed\n'),
|
||||
('bearer', ''): (401,
|
||||
{},
|
||||
{'error': {'code': 'authorization_required',
|
||||
'message': 'Authorization Bearer token is required'}}),
|
||||
('bearer', 'Basic bad'): (401,
|
||||
{},
|
||||
{'error': {'code': 'authorization_required',
|
||||
'message': 'Authorization Bearer token is '
|
||||
'required'}}),
|
||||
('bearer', 'Bearer '): (401,
|
||||
{},
|
||||
{'error': {'code': 'authorization_required',
|
||||
'message': 'Authorization Bearer token is required'}}),
|
||||
('bearer', 'Bearer bad'): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_token',
|
||||
'message': 'Access token is invalid or expired'}}),
|
||||
('bearer', 'bearer bad'): (401,
|
||||
{},
|
||||
{'error': {'code': 'authorization_required',
|
||||
'message': 'Authorization Bearer token is '
|
||||
'required'}}),
|
||||
('login', 0): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email or password is incorrect'}}),
|
||||
('login', 1): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email or password is incorrect'}}),
|
||||
('login', 2): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email or password is incorrect'}}),
|
||||
('login', 3): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email or password is incorrect'}}),
|
||||
('login', 4): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('login', 5): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email or password is incorrect'}}),
|
||||
('login', 6): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('logout JSON', 0): (204, {}, b''),
|
||||
('logout JSON', 1): (204, {}, b''),
|
||||
('logout JSON', 2): (204, {}, b''),
|
||||
('logout JSON', 3): (204, {}, b''),
|
||||
('logout JSON', 4): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('logout JSON', 5): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('password boundary', 0): 201,
|
||||
('password boundary', 1): 201,
|
||||
('password boundary', 2): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_password',
|
||||
'message': 'password is too long'}}),
|
||||
('path', '/%2e%2e/app.js'): (200,
|
||||
{'cache-control': 'public, max-age=3600',
|
||||
'content-type': 'text/javascript; charset=utf-8'},
|
||||
('web asset', 'app.js')),
|
||||
('path', '/%61pp.js'): (200,
|
||||
{'cache-control': 'public, max-age=3600',
|
||||
'content-type': 'text/javascript; charset=utf-8'},
|
||||
('web asset', 'app.js')),
|
||||
('path', '//app.js'): (307,
|
||||
{'content-type': 'text/html; charset=utf-8',
|
||||
'location': '/app.js'},
|
||||
b'<a href="/app.js">Temporary Redirect</a>.\n\n'),
|
||||
('path', '/a/../app.js'): (307,
|
||||
{'content-type': 'text/html; charset=utf-8',
|
||||
'location': '/app.js'},
|
||||
b'<a href="/app.js">Temporary Redirect</a>.\n\n'),
|
||||
('path', '/app.js/'): (301,
|
||||
{'cache-control': 'public, max-age=3600', 'location': '../app.js'},
|
||||
b''),
|
||||
('path', '/index.html'): (301, {'cache-control': 'no-cache', 'location': './'}, b''),
|
||||
('path', '/v1'): (404,
|
||||
{'content-type': 'text/plain; charset=utf-8'},
|
||||
b'404 page not found\n'),
|
||||
('path', '/v1/'): (404,
|
||||
{'cache-control': 'no-store',
|
||||
'content-type': 'application/json; charset=utf-8'},
|
||||
{'error': {'code': 'not_found', 'message': 'Endpoint not found'}}),
|
||||
('path', '/v1/../app.js'): (307,
|
||||
{'cache-control': 'no-store',
|
||||
'content-type': 'text/html; charset=utf-8',
|
||||
'location': '/app.js'},
|
||||
b'<a href="/app.js">Temporary Redirect</a>.\n\n'),
|
||||
('path', '/v1//me'): (307,
|
||||
{'cache-control': 'no-store',
|
||||
'content-type': 'text/html; charset=utf-8',
|
||||
'location': '/v1/me'},
|
||||
b'<a href="/v1/me">Temporary Redirect</a>.\n\n'),
|
||||
('range', 'bytes=-10'): (206,
|
||||
{'accept-ranges': 'bytes',
|
||||
'content-range': 'bytes 1112-1121/1122',
|
||||
'content-type': 'image/svg+xml'},
|
||||
b'/>\n</svg>\n'),
|
||||
('range', 'bytes=0-0,2-2'): (200,
|
||||
{'accept-ranges': 'bytes', 'content-type': 'image/svg+xml'},
|
||||
('web asset', 'icon.svg')),
|
||||
('range', 'bytes=0-9'): (206,
|
||||
{'accept-ranges': 'bytes',
|
||||
'content-range': 'bytes 0-9/1122',
|
||||
'content-type': 'image/svg+xml'},
|
||||
b'<svg xmlns'),
|
||||
('range', 'bytes=10-'): (206,
|
||||
{'accept-ranges': 'bytes',
|
||||
'content-range': 'bytes 10-1121/1122',
|
||||
'content-type': 'image/svg+xml'},
|
||||
b'="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="im'
|
||||
b'g" aria-labelledby="title">\n <title id="title">Balance</tit'
|
||||
b'le>\n <defs>\n <linearGradient id="bg" x1="70" y1="36" x2='
|
||||
b'"446" y2="476" gradientUnits="userSpaceOnUse">\n <stop s'
|
||||
b'top-color="#7777ef"/>\n <stop offset="1" stop-color="#42'
|
||||
b'42b8"/>\n </linearGradient>\n <linearGradient id="shine"'
|
||||
b' x1="144" y1="122" x2="366" y2="386" gradientUnits="userSpac'
|
||||
b'eOnUse">\n <stop stop-color="#fff"/>\n <stop offset='
|
||||
b'"1" stop-color="#dedfff"/>\n </linearGradient>\n </def'
|
||||
b's>\n <rect x="28" y="28" width="456" height="456" rx="128" f'
|
||||
b'ill="url(#bg)"/>\n <path d="M141 173c0-25 20-45 45-45h140c25'
|
||||
b' 0 45 20 45 45v18H186c-25 0-45 20-45 45v-63Z" fill="#fff" op'
|
||||
b'acity=".5"/>\n <path d="M116 222c0-28 23-51 51-51h179c28 0 5'
|
||||
b'1 23 51 51v126c0 28-23 51-51 51H167c-28 0-51-23-51-51V222Z" '
|
||||
b'fill="url(#shine)"/>\n <path d="M302 258h95v86h-95c-24 0-43-'
|
||||
b'19-43-43s19-43 43-43Z" fill="#5b5bd6"/>\n <circle cx="306" c'
|
||||
b'y="301" r="13" fill="#fff"/>\n <path d="M184 250h44M184 289h'
|
||||
b'44M184 328h44" stroke="#5b5bd6" stroke-width="18" stroke-lin'
|
||||
b'ecap="round" opacity=".72"/>\n</svg>\n'),
|
||||
('range', 'bytes=10-1'): (416,
|
||||
{'content-type': 'text/plain; charset=utf-8'},
|
||||
b'invalid range\n'),
|
||||
('range', 'bytes=99999999-'): (416,
|
||||
{'content-range': 'bytes */1122',
|
||||
'content-type': 'text/plain; charset=utf-8'},
|
||||
b'invalid range: failed to overlap\n'),
|
||||
('range', 'bytes=bad'): (416,
|
||||
{'content-type': 'text/plain; charset=utf-8'},
|
||||
b'invalid range\n'),
|
||||
('range', 'items=0-1'): (416,
|
||||
{'content-type': 'text/plain; charset=utf-8'},
|
||||
b'invalid range\n'),
|
||||
('raw sync', 0): (200, {}, {'changes': [], 'cursor': 0, 'hasMore': False}),
|
||||
('raw sync', 1): (200, {}, {'changes': [], 'cursor': 0, 'hasMore': False}),
|
||||
('raw sync', 2): (200, {}, {'changes': [], 'cursor': 0, 'hasMore': False}),
|
||||
('raw sync', 3): (200, {}, {'changes': [], 'cursor': 0, 'hasMore': False}),
|
||||
('raw sync', 4): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('raw sync', 5): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('raw sync', 6): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'entity must be transaction, category, or '
|
||||
'budget'}}),
|
||||
('raw sync', 7): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('raw sync', 8): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('raw sync', 9): (200, {}, {'changes': [], 'cursor': 0, 'hasMore': False}),
|
||||
('raw sync', 10): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('refresh JSON', 0): (400,
|
||||
{},
|
||||
{'error': {'code': 'refresh_token_required',
|
||||
'message': 'Refresh token is required'}}),
|
||||
('refresh JSON', 1): (400,
|
||||
{},
|
||||
{'error': {'code': 'refresh_token_required',
|
||||
'message': 'Refresh token is required'}}),
|
||||
('refresh JSON', 2): (400,
|
||||
{},
|
||||
{'error': {'code': 'refresh_token_required',
|
||||
'message': 'Refresh token is required'}}),
|
||||
('refresh JSON', 3): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_refresh_token',
|
||||
'message': 'Refresh token is invalid or expired'}}),
|
||||
('refresh JSON', 4): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('refresh JSON', 5): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('register', 0): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_email',
|
||||
'message': 'Enter a valid email address'}}),
|
||||
('register', 1): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_email',
|
||||
'message': 'Enter a valid email address'}}),
|
||||
('register', 2): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_email',
|
||||
'message': 'Enter a valid email address'}}),
|
||||
('register', 3): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_email',
|
||||
'message': 'Enter a valid email address'}}),
|
||||
('register', 4): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('register', 5): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_email',
|
||||
'message': 'Enter a valid email address'}}),
|
||||
('register', 6): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('signed claims', 0): (200, {}, {'email': 'e', 'id': 'u'}),
|
||||
('signed claims', 1): (200, {}, {'email': 'e', 'id': 'u'}),
|
||||
('signed claims', 2): (200, {}, {'email': '', 'id': 'u'}),
|
||||
('signed claims', 3): (200, {}, {'email': '', 'id': 'u'}),
|
||||
('signed claims', 4): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_token',
|
||||
'message': 'Access token is invalid or expired'}}),
|
||||
('signed claims', 5): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_token',
|
||||
'message': 'Access token is invalid or expired'}}),
|
||||
('signed claims', 6): (401,
|
||||
{},
|
||||
{'error': {'code': 'invalid_token',
|
||||
'message': 'Access token is invalid or expired'}}),
|
||||
('sync validation', 0): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'entity must be transaction, category, or '
|
||||
'budget'}}),
|
||||
('sync validation', 1): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'record id is invalid'}}),
|
||||
('sync validation', 2): (200,
|
||||
{},
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1}],
|
||||
'cursor': 1,
|
||||
'hasMore': False}),
|
||||
('sync validation', 3): (200,
|
||||
{},
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1}],
|
||||
'cursor': 1,
|
||||
'hasMore': False}),
|
||||
('sync validation', 4): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'updatedAt is required'}}),
|
||||
('sync validation', 5): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('sync validation', 6): (200,
|
||||
{},
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1}],
|
||||
'cursor': 1,
|
||||
'hasMore': False}),
|
||||
('sync validation', 7): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}}),
|
||||
('sync validation', 8): (200,
|
||||
{},
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1},
|
||||
{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': {'amount': 125.5,
|
||||
'nested': [None, True, 1],
|
||||
'note': 'Тест €'},
|
||||
'sequence': 2,
|
||||
'updatedAt': '2025-01-01T00:00:00.123456789Z',
|
||||
'version': 2}],
|
||||
'cursor': 2,
|
||||
'hasMore': False}),
|
||||
('sync validation', 9): (200,
|
||||
{},
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1},
|
||||
{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': {'amount': 125.5,
|
||||
'nested': [None, True, 1],
|
||||
'note': 'Тест €'},
|
||||
'sequence': 2,
|
||||
'updatedAt': '2025-01-01T00:00:00.123456789Z',
|
||||
'version': 2}],
|
||||
'cursor': 2,
|
||||
'hasMore': False}),
|
||||
('sync validation', 10): (200,
|
||||
{},
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1},
|
||||
{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': {'amount': 125.5,
|
||||
'nested': [None, True, 1],
|
||||
'note': 'Тест €'},
|
||||
'sequence': 2,
|
||||
'updatedAt': '2025-01-01T00:00:00.123456789Z',
|
||||
'version': 2}],
|
||||
'cursor': 2,
|
||||
'hasMore': False}),
|
||||
('sync validation', 11): (200,
|
||||
{},
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1},
|
||||
{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': {'amount': 125.5,
|
||||
'nested': [None, True, 1],
|
||||
'note': 'Тест €'},
|
||||
'sequence': 2,
|
||||
'updatedAt': '2025-01-01T00:00:00.123456789Z',
|
||||
'version': 2}],
|
||||
'cursor': 2,
|
||||
'hasMore': False}),
|
||||
('sync validation', 12): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'updatedAt is too far in the future'}}),
|
||||
('sync validation', 13): (400,
|
||||
{},
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'payload is too large'}})},
|
||||
'raw': {'raw database payloads': [(b'wire-0',
|
||||
b'{ "duplicate":1, "duplicate":2, "n":123456789012345678901234'
|
||||
b'567890, "big":1e999 }'),
|
||||
(b'wire-1',
|
||||
b'{"lone":"\\ud800","low":"\\udfff","pair":"\\ud83d\\ude00"}'),
|
||||
(b'wire-2', b'{"invalid":"\xff\xc0\xed\xa0\x80"}'),
|
||||
(b'wire-3',
|
||||
b'{"html":"<>&","separator":"\xe2\x80\xa8\xe2\x80\xa9"}'),
|
||||
(b'wire-4',
|
||||
b'{"nested":[{"payload":{"id":"inner"}},"quote\\"end",null,true'
|
||||
b']}'),
|
||||
(b'\xef\xbf\xbd', b'{}')],
|
||||
('unicode credentials', 0): (401,
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email or password is incorrect'}}),
|
||||
('unicode credentials', 1): (401,
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email or password is incorrect'}}),
|
||||
('unicode id', 0): {'changes': [{'deleted': False,
|
||||
'entity': 'category',
|
||||
'id': '�',
|
||||
'payload': {},
|
||||
'sequence': 6,
|
||||
'updatedAt': '2025-01-02T00:00:00Z',
|
||||
'version': 1}],
|
||||
'cursor': 6,
|
||||
'hasMore': False},
|
||||
('unicode id', 1): {'changes': [], 'cursor': 6, 'hasMore': False},
|
||||
('wire payload', 0): b'{"duplicate":1,"duplicate":2,"n":123456789012345678901234567890,'
|
||||
b'"big":1e999}',
|
||||
('wire payload', 1): b'{"lone":"\\ud800","low":"\\udfff","pair":"\\ud83d\\ude00"}',
|
||||
('wire payload', 2): b'{"invalid":"\xff\xc0\xed\xa0\x80"}',
|
||||
('wire payload', 3): b'{"html":"\\u003c\\u003e\\u0026","separator":"\\u2028\\u2029"}',
|
||||
('wire payload', 4): b'{"nested":[{"payload":{"id":"inner"}},"quote\\"end",null,true]}'},
|
||||
'scenario': {'/': (200,
|
||||
{'cache-control': 'no-cache',
|
||||
'content-security-policy': "default-src 'self'; base-uri 'none'; connect-src "
|
||||
"'self'; font-src 'self'; form-action 'self'; "
|
||||
"frame-ancestors 'none'; img-src 'self' data:; "
|
||||
"manifest-src 'self'; script-src 'self'; style-src "
|
||||
"'self'; worker-src 'self'",
|
||||
'content-type': 'text/html; charset=utf-8',
|
||||
'permissions-policy': 'camera=(), microphone=(), geolocation=(), payment=()',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-frame-options': 'DENY'},
|
||||
('web asset', 'index.html')),
|
||||
'/app.js': (200,
|
||||
{'cache-control': 'public, max-age=3600',
|
||||
'content-security-policy': "default-src 'self'; base-uri 'none'; "
|
||||
"connect-src 'self'; font-src 'self'; "
|
||||
"form-action 'self'; frame-ancestors 'none'; "
|
||||
"img-src 'self' data:; manifest-src 'self'; "
|
||||
"script-src 'self'; style-src 'self'; "
|
||||
"worker-src 'self'",
|
||||
'content-type': 'text/javascript; charset=utf-8',
|
||||
'permissions-policy': 'camera=(), microphone=(), geolocation=(), '
|
||||
'payment=()',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-frame-options': 'DENY'},
|
||||
('web asset', 'app.js')),
|
||||
'/icon.svg': (200,
|
||||
{'cache-control': 'public, max-age=3600',
|
||||
'content-security-policy': "default-src 'self'; base-uri 'none'; "
|
||||
"connect-src 'self'; font-src 'self'; "
|
||||
"form-action 'self'; frame-ancestors "
|
||||
"'none'; img-src 'self' data:; "
|
||||
"manifest-src 'self'; script-src 'self'; "
|
||||
"style-src 'self'; worker-src 'self'",
|
||||
'content-type': 'image/svg+xml',
|
||||
'permissions-policy': 'camera=(), microphone=(), geolocation=(), '
|
||||
'payment=()',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-frame-options': 'DENY'},
|
||||
('web asset', 'icon.svg')),
|
||||
'/manifest.webmanifest': (200,
|
||||
{'cache-control': 'public, max-age=3600',
|
||||
'content-security-policy': "default-src 'self'; base-uri "
|
||||
"'none'; connect-src 'self'; "
|
||||
"font-src 'self'; form-action "
|
||||
"'self'; frame-ancestors "
|
||||
"'none'; img-src 'self' data:; "
|
||||
"manifest-src 'self'; "
|
||||
"script-src 'self'; style-src "
|
||||
"'self'; worker-src 'self'",
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
'permissions-policy': 'camera=(), microphone=(), '
|
||||
'geolocation=(), payment=()',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-frame-options': 'DENY'},
|
||||
('web asset', 'manifest.webmanifest')),
|
||||
'/missing-file': (404,
|
||||
{'cache-control': None,
|
||||
'content-security-policy': "default-src 'self'; base-uri 'none'; "
|
||||
"connect-src 'self'; font-src 'self'; "
|
||||
"form-action 'self'; frame-ancestors "
|
||||
"'none'; img-src 'self' data:; "
|
||||
"manifest-src 'self'; script-src "
|
||||
"'self'; style-src 'self'; worker-src "
|
||||
"'self'",
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
'permissions-policy': 'camera=(), microphone=(), geolocation=(), '
|
||||
'payment=()',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-frame-options': 'DENY'},
|
||||
b'404 page not found\n'),
|
||||
'/service-worker.js': (200,
|
||||
{'cache-control': 'no-cache',
|
||||
'content-security-policy': "default-src 'self'; base-uri "
|
||||
"'none'; connect-src 'self'; "
|
||||
"font-src 'self'; form-action "
|
||||
"'self'; frame-ancestors 'none'; "
|
||||
"img-src 'self' data:; "
|
||||
"manifest-src 'self'; script-src "
|
||||
"'self'; style-src 'self'; "
|
||||
"worker-src 'self'",
|
||||
'content-type': 'text/javascript; charset=utf-8',
|
||||
'permissions-policy': 'camera=(), microphone=(), '
|
||||
'geolocation=(), payment=()',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-frame-options': 'DENY'},
|
||||
('web asset', 'service-worker.js')),
|
||||
'/styles.css': (200,
|
||||
{'cache-control': 'public, max-age=3600',
|
||||
'content-security-policy': "default-src 'self'; base-uri 'none'; "
|
||||
"connect-src 'self'; font-src 'self'; "
|
||||
"form-action 'self'; frame-ancestors "
|
||||
"'none'; img-src 'self' data:; "
|
||||
"manifest-src 'self'; script-src 'self'; "
|
||||
"style-src 'self'; worker-src 'self'",
|
||||
'content-type': 'text/css; charset=utf-8',
|
||||
'permissions-policy': 'camera=(), microphone=(), geolocation=(), '
|
||||
'payment=()',
|
||||
'referrer-policy': 'no-referrer',
|
||||
'x-content-type-options': 'nosniff',
|
||||
'x-frame-options': 'DENY'},
|
||||
('web asset', 'styles.css')),
|
||||
'GET /v1/me None': (401,
|
||||
{'error': {'code': 'authorization_required',
|
||||
'message': 'Authorization Bearer token is required'}}),
|
||||
'GET /v1/missing None': (404,
|
||||
{'error': {'code': 'not_found',
|
||||
'message': 'Endpoint not found'}}),
|
||||
"POST /v1/auth/login {'email': 'User@Example.COM', 'password': 'compatibility password'}": (401,
|
||||
{'error': {'code': 'invalid_credentials',
|
||||
'message': 'Email '
|
||||
'or '
|
||||
'password '
|
||||
'is '
|
||||
'incorrect'}}),
|
||||
"POST /v1/auth/login {'unknown': 1}": (400,
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not '
|
||||
'valid JSON'}}),
|
||||
"POST /v1/auth/refresh {'refreshToken': 'bad'}": (401,
|
||||
{'error': {'code': 'invalid_refresh_token',
|
||||
'message': 'Refresh '
|
||||
'token is '
|
||||
'invalid or '
|
||||
'expired'}}),
|
||||
'POST /v1/auth/refresh {}': (400,
|
||||
{'error': {'code': 'refresh_token_required',
|
||||
'message': 'Refresh token is required'}}),
|
||||
"POST /v1/auth/register {'email': 'a@b', 'password': 'short'}": (400,
|
||||
{'error': {'code': 'invalid_password',
|
||||
'message': 'password '
|
||||
'must '
|
||||
'contain '
|
||||
'at '
|
||||
'least '
|
||||
'8 '
|
||||
'characters'}}),
|
||||
"POST /v1/auth/register {'email': 'bad', 'password': 'compatibility password'}": (400,
|
||||
{'error': {'code': 'invalid_email',
|
||||
'message': 'Enter '
|
||||
'a '
|
||||
'valid '
|
||||
'email '
|
||||
'address'}}),
|
||||
'deleted': {'changes': [{'deleted': True,
|
||||
'entity': 'transaction',
|
||||
'id': 'one',
|
||||
'sequence': 5,
|
||||
'updatedAt': '2025-01-02T00:00:00Z',
|
||||
'version': 2}],
|
||||
'cursor': 5,
|
||||
'hasMore': False},
|
||||
'duplicate': {'error': {'code': 'email_exists',
|
||||
'message': 'An account with this email already exists'}},
|
||||
'expired access': {'error': {'code': 'invalid_token',
|
||||
'message': 'Access token is invalid or expired'}},
|
||||
'first': {'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1},
|
||||
{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'one',
|
||||
'payload': {'amount': 125.5,
|
||||
'nested': [None, True, 1],
|
||||
'note': 'Тест €'},
|
||||
'sequence': 2,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1},
|
||||
{'deleted': False,
|
||||
'entity': 'category',
|
||||
'id': 'cat',
|
||||
'payload': {'amount': 125.5,
|
||||
'nested': [None, True, 1],
|
||||
'note': 'Тест €'},
|
||||
'sequence': 3,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1},
|
||||
{'deleted': False,
|
||||
'entity': 'budget',
|
||||
'id': 'budget',
|
||||
'payload': {'amount': 125.5,
|
||||
'nested': [None, True, 1],
|
||||
'note': 'Тест €'},
|
||||
'sequence': 4,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1}],
|
||||
'cursor': 4,
|
||||
'hasMore': False},
|
||||
'https://allowed.example': (204,
|
||||
{'access-control-allow-headers': 'Authorization, '
|
||||
'Content-Type',
|
||||
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
||||
'access-control-allow-origin': 'https://allowed.example',
|
||||
'vary': 'Origin'}),
|
||||
'https://denied.example': (204, {}),
|
||||
'idempotent': {'changes': [], 'cursor': 4, 'hasMore': False},
|
||||
'invalid bearer': {'error': {'code': 'invalid_token',
|
||||
'message': 'Access token is invalid or expired'}},
|
||||
"invalid sync {'changes': [{'entity': 'category', 'id': 'x'}]}": (400,
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'updatedAt '
|
||||
'is '
|
||||
'required'}}),
|
||||
"invalid sync {'changes': [{'entity': 'transaction', 'id': '', 'updatedAt': '2025-01-01T00:00:00Z', 'payload': {'amount': 125.5, 'note': 'Тест €', 'nested': [None, True, 1]}}]}": (400,
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'record '
|
||||
'id '
|
||||
'is '
|
||||
'invalid'}}),
|
||||
"invalid sync {'changes': [{'entity': 'transaction', 'id': 'x', 'updatedAt': '2025-01-01T00:00:00Z', 'payload': None}]}": (200,
|
||||
{'changes': [{'deleted': False,
|
||||
'entity': 'transaction',
|
||||
'id': 'x',
|
||||
'payload': None,
|
||||
'sequence': 1,
|
||||
'updatedAt': '2025-01-01T00:00:00Z',
|
||||
'version': 1}],
|
||||
'cursor': 1,
|
||||
'hasMore': False}),
|
||||
"invalid sync {'changes': [{'entity': 'unknown', 'id': 'x', 'updatedAt': '2025-01-01T00:00:00Z', 'payload': {'amount': 125.5, 'note': 'Тест €', 'nested': [None, True, 1]}}]}": (400,
|
||||
{'error': {'code': 'invalid_change',
|
||||
'message': 'entity '
|
||||
'must '
|
||||
'be '
|
||||
'transaction, '
|
||||
'category, '
|
||||
'or '
|
||||
'budget'}}),
|
||||
"invalid sync {'cursor': -1}": (400,
|
||||
{'error': {'code': 'invalid_sync_request',
|
||||
'message': 'Cursor or change count is '
|
||||
'invalid'}}),
|
||||
"invalid sync {'extra': 1}": (400,
|
||||
{'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid '
|
||||
'JSON'}}),
|
||||
'logout refresh': {'error': {'code': 'invalid_refresh_token',
|
||||
'message': 'Refresh token is invalid or expired'}},
|
||||
'malformed': {'error': {'code': 'invalid_json',
|
||||
'message': 'Request body is not valid JSON'}},
|
||||
'pagination': (505, 510),
|
||||
'rate limit': {'error': {'code': 'rate_limited',
|
||||
'message': 'Too many authentication requests'}},
|
||||
'refresh replay': {'error': {'code': 'invalid_refresh_token',
|
||||
'message': 'Refresh token is invalid or expired'}},
|
||||
'registration disabled': {'error': {'code': 'registration_disabled',
|
||||
'message': 'Registration is disabled'}}}}
|
||||
Reference in new issue
Block a user