memos-mcp 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/.vscode/settings.json +0 -19
- package/AGENTS.md +0 -96
- package/Jenkinsfile +0 -80
- package/RELEASE.md +0 -25
- package/eslint.config.js +0 -34
- package/memory.md +0 -51
- package/src/cert-manager.test.ts +0 -322
- package/src/memos-client.test.ts +0 -312
- package/tsconfig.json +0 -25
package/package.json
CHANGED
package/.vscode/settings.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"[typescript]": {
|
|
3
|
-
"editor.formatOnSave": true,
|
|
4
|
-
"editor.formatOnSaveMode": "file"
|
|
5
|
-
},
|
|
6
|
-
"editor.formatOnSave": true,
|
|
7
|
-
"editor.formatOnSaveMode": "file",
|
|
8
|
-
"editor.codeActionsOnSave": {
|
|
9
|
-
"source.fixAll.biome": "explicit",
|
|
10
|
-
"source.organizeImports.biome": "explicit"
|
|
11
|
-
},
|
|
12
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
13
|
-
"nodejs-testing.extensions": [
|
|
14
|
-
{
|
|
15
|
-
"extensions": ["mjs", "cjs", "js", "ts"],
|
|
16
|
-
"parameters": []
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
}
|
package/AGENTS.md
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# Agent Guidelines for memos-mcp
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This is an MCP (Model Context Protocol) server for Memos - a lightweight, self-hosted memo hub. The server provides tools to create and search memos via the Memos API.
|
|
6
|
-
|
|
7
|
-
## Required Checks Before Committing
|
|
8
|
-
|
|
9
|
-
**IMPORTANT:** Always run these commands before committing changes:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# 1. Type checking - REQUIRED
|
|
13
|
-
npm run typecheck
|
|
14
|
-
|
|
15
|
-
# 2. Linting - REQUIRED
|
|
16
|
-
npm run lint
|
|
17
|
-
|
|
18
|
-
# 3. Format check - REQUIRED
|
|
19
|
-
npm run format:check
|
|
20
|
-
|
|
21
|
-
# 4. Tests - REQUIRED
|
|
22
|
-
npm run test
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
All checks must pass before committing. You can auto-fix lint and format issues:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm run lint:fix # Auto-fix lint issues
|
|
29
|
-
npm run format # Auto-format code
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Project Structure
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
src/
|
|
36
|
-
├── index.ts # MCP server entry point
|
|
37
|
-
├── memos-client.ts # Memos API client
|
|
38
|
-
├── memos-client.test.ts # Client tests
|
|
39
|
-
├── cert-manager.ts # SSL certificate auto-fetch & cache
|
|
40
|
-
└── cert-manager.test.ts # Cert manager tests
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Key Technical Details
|
|
44
|
-
|
|
45
|
-
### SSL Certificate Handling
|
|
46
|
-
|
|
47
|
-
The project includes automatic SSL certificate management for servers with incomplete certificate chains (missing intermediates). See `cert-manager.ts`:
|
|
48
|
-
|
|
49
|
-
- Auto-fetches Let's Encrypt intermediate certificates via HTTPS
|
|
50
|
-
- Caches certificates in `~/.cache/memos-mcp/certs/`
|
|
51
|
-
- Auto-refreshes when cache > 30 days or cert expires within 30 days
|
|
52
|
-
|
|
53
|
-
### Dependencies
|
|
54
|
-
|
|
55
|
-
- `@modelcontextprotocol/sdk` - MCP SDK for server implementation
|
|
56
|
-
- `undici` - HTTP client with custom TLS agent support
|
|
57
|
-
|
|
58
|
-
### Node.js Version
|
|
59
|
-
|
|
60
|
-
Requires Node.js >= 22.0.0
|
|
61
|
-
|
|
62
|
-
## Code Style
|
|
63
|
-
|
|
64
|
-
- Uses Prettier for formatting (config in package.json)
|
|
65
|
-
- Uses ESLint with TypeScript plugin
|
|
66
|
-
- Double quotes for strings
|
|
67
|
-
- Semicolons required
|
|
68
|
-
- 120 character line width
|
|
69
|
-
|
|
70
|
-
## Testing
|
|
71
|
-
|
|
72
|
-
Tests use Node.js built-in test runner (`node:test`):
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
npm run test
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Integration tests in `cert-manager.test.ts` make real network requests to Let's Encrypt servers.
|
|
79
|
-
|
|
80
|
-
## Common Tasks
|
|
81
|
-
|
|
82
|
-
### Adding a new MCP tool
|
|
83
|
-
|
|
84
|
-
1. Add tool definition in `src/index.ts` under `server.setRequestHandler(ListToolsRequestSchema, ...)`
|
|
85
|
-
2. Add tool handler in `server.setRequestHandler(CallToolRequestSchema, ...)`
|
|
86
|
-
3. Implement the method in `src/memos-client.ts`
|
|
87
|
-
4. Add tests in `src/memos-client.test.ts`
|
|
88
|
-
|
|
89
|
-
### Updating certificates
|
|
90
|
-
|
|
91
|
-
Certificates are auto-fetched, but you can force refresh:
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
import { refreshAllCerts } from "./cert-manager.ts";
|
|
95
|
-
await refreshAllCerts();
|
|
96
|
-
```
|
package/Jenkinsfile
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
pipeline {
|
|
2
|
-
agent any
|
|
3
|
-
tools {
|
|
4
|
-
nodejs 'NodeJS 24.x'
|
|
5
|
-
}
|
|
6
|
-
stages {
|
|
7
|
-
stage('Checkout') {
|
|
8
|
-
steps {
|
|
9
|
-
checkout scm
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
stage('Install') {
|
|
14
|
-
steps {
|
|
15
|
-
script {
|
|
16
|
-
sh 'npm i'
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
stage('Lint') {
|
|
22
|
-
steps {
|
|
23
|
-
script {
|
|
24
|
-
sh 'npm run lint --if-present'
|
|
25
|
-
sh 'npm run format:check --if-present'
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
stage('Test') {
|
|
31
|
-
steps {
|
|
32
|
-
script {
|
|
33
|
-
sh 'npm test --if-present'
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
stage('Build') {
|
|
39
|
-
steps {
|
|
40
|
-
script {
|
|
41
|
-
sh 'npm run build --if-present'
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
stage('Publish Official Release') {
|
|
47
|
-
when {
|
|
48
|
-
expression { env.TAG_NAME != null } // Only run for tag builds
|
|
49
|
-
}
|
|
50
|
-
environment {
|
|
51
|
-
NPM_TOKEN = credentials('npm-verdaccio-publish-token')
|
|
52
|
-
}
|
|
53
|
-
steps {
|
|
54
|
-
sh 'npm config set //verdaccio-verdaccio-68mj54-e6081a-192-168-111-123.traefik.me/:_authToken=${NPM_TOKEN} --location project'
|
|
55
|
-
sh 'npm publish --tag latest'
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// stage('Publish Snapshot Build') {
|
|
60
|
-
// when {
|
|
61
|
-
// expression { env.TAG_NAME == null } // Only run for non-tag builds
|
|
62
|
-
// }
|
|
63
|
-
// environment {
|
|
64
|
-
// NPM_TOKEN = credentials('npm-verdaccio-publish-token')
|
|
65
|
-
// }
|
|
66
|
-
// steps {
|
|
67
|
-
// sh 'npm config set //verdaccio-verdaccio-68mj54-e6081a-192-168-111-123.traefik.me/:_authToken=${NPM_TOKEN} --location project'
|
|
68
|
-
// sh 'npm version 0.0.0-BUILD-${BUILD_NUMBER} --no-git-tag-version' // Apply snapshot version
|
|
69
|
-
// sh 'npm publish --tag snapshot' // Publish with snapshot tag
|
|
70
|
-
// }
|
|
71
|
-
// }
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
post {
|
|
76
|
-
always {
|
|
77
|
-
cleanWs()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
package/RELEASE.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
## Release Process
|
|
2
|
-
|
|
3
|
-
This project uses a two-pronged release approach:
|
|
4
|
-
|
|
5
|
-
### Official Releases (Versioned)
|
|
6
|
-
|
|
7
|
-
Official releases follow Semantic Versioning (MAJOR.MINOR.PATCH) and are published to npm with the `latest` tag.
|
|
8
|
-
|
|
9
|
-
**Steps for a Developer to Create an Official Release:**
|
|
10
|
-
|
|
11
|
-
1. **Ensure your local `main` branch is up-to-date** and all changes intended for the release are merged.
|
|
12
|
-
2. **Run `npm version <patch|minor|major>` locally.** This command will:
|
|
13
|
-
- Increment the version in `package.json`.
|
|
14
|
-
- Create a git commit for the version change.
|
|
15
|
-
- Create a corresponding local git tag (e.g., `v1.0.0`).
|
|
16
|
-
- Example: `npm version patch`
|
|
17
|
-
3. **Push the commit and the new tag to the remote repository:**
|
|
18
|
-
```bash
|
|
19
|
-
git push && git push --tags
|
|
20
|
-
```
|
|
21
|
-
4. **Jenkins will automatically detect the new tag** and trigger a pipeline build. The `Publish Official Release` stage in the `Jenkinsfile` will then publish the package to npm with the `latest` tag.
|
|
22
|
-
|
|
23
|
-
### Snapshot Builds
|
|
24
|
-
|
|
25
|
-
For builds on branches that are not triggered by a git tag (e.g., `main` branch builds from regular commits), a snapshot version is automatically applied and published to npm with a `snapshot` tag. These builds are primarily for continuous integration and testing purposes and do not represent official releases.
|
package/eslint.config.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// eslint.config.js
|
|
2
|
-
import js from "@eslint/js";
|
|
3
|
-
import typescript from "@typescript-eslint/eslint-plugin";
|
|
4
|
-
import typescriptParser from "@typescript-eslint/parser";
|
|
5
|
-
import prettier from "eslint-plugin-prettier";
|
|
6
|
-
import prettierConfig from "eslint-config-prettier";
|
|
7
|
-
import globals from "globals";
|
|
8
|
-
|
|
9
|
-
export default [
|
|
10
|
-
js.configs.recommended,
|
|
11
|
-
{
|
|
12
|
-
files: ["**/*.{js,mjs,cjs,ts,tsx}"],
|
|
13
|
-
languageOptions: {
|
|
14
|
-
ecmaVersion: "latest",
|
|
15
|
-
sourceType: "module",
|
|
16
|
-
parser: typescriptParser,
|
|
17
|
-
globals: {
|
|
18
|
-
...globals.node,
|
|
19
|
-
RequestInit: "readonly",
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
plugins: {
|
|
23
|
-
"@typescript-eslint": typescript,
|
|
24
|
-
prettier: prettier,
|
|
25
|
-
},
|
|
26
|
-
rules: {
|
|
27
|
-
...typescript.configs.recommended.rules,
|
|
28
|
-
...prettierConfig.rules,
|
|
29
|
-
"prettier/prettier": "error",
|
|
30
|
-
"no-unused-vars": "off",
|
|
31
|
-
"@typescript-eslint/no-unused-vars": "error",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
];
|
package/memory.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Memory
|
|
2
|
-
|
|
3
|
-
## Project Purpose
|
|
4
|
-
|
|
5
|
-
This project appears to be a TypeScript project template, providing a basic setup for developing and testing TypeScript code. It includes a simple `add` function and a corresponding unit test.
|
|
6
|
-
|
|
7
|
-
## Project Structure
|
|
8
|
-
|
|
9
|
-
- `.gitignore`: Specifies intentionally untracked files to ignore.
|
|
10
|
-
- `eslint.config.js`: ESLint configuration for linting TypeScript files.
|
|
11
|
-
- `Jenkinsfile`: Likely a Jenkins pipeline definition for CI/CD.
|
|
12
|
-
- `package-lock.json`: Records the exact dependency tree.
|
|
13
|
-
- `package.json`: Project metadata, scripts, and dependencies.
|
|
14
|
-
- `README.md`: Project README file.
|
|
15
|
-
- `RELEASE.md`: Release notes or guidelines.
|
|
16
|
-
- `src/`: Source code directory.
|
|
17
|
-
- `src/index.ts`: Contains the main application logic (currently a simple `add` function).
|
|
18
|
-
- `test/`: Test code directory.
|
|
19
|
-
- `test/index.test.ts`: Contains unit tests for `src/index.ts`.
|
|
20
|
-
|
|
21
|
-
## Project Technical Details
|
|
22
|
-
|
|
23
|
-
### Technical Stack
|
|
24
|
-
|
|
25
|
-
- **Language**: Node.js Native TypeScript (means not compiler required and must import module with `.ts`/`.js` suffix)
|
|
26
|
-
- **Runtime**: Node.js
|
|
27
|
-
- **Linting**: ESLint with `@typescript-eslint/eslint-plugin` and `@typescript-eslint/parser`
|
|
28
|
-
- **Formatting**: Prettier with `eslint-config-prettier` and `eslint-plugin-prettier`
|
|
29
|
-
- **Testing**: Node.js built-in test runner (`node --test`)
|
|
30
|
-
|
|
31
|
-
### Important Dependent Libraries
|
|
32
|
-
|
|
33
|
-
- `@types/node`: Type definitions for Node.js.
|
|
34
|
-
- `@typescript-eslint/eslint-plugin`: ESLint plugin for TypeScript.
|
|
35
|
-
- `@typescript-eslint/parser`: Parser for ESLint to understand TypeScript.
|
|
36
|
-
- `eslint`: Linter.
|
|
37
|
-
- `eslint-config-prettier`: Disables ESLint rules that conflict with Prettier.
|
|
38
|
-
- `eslint-plugin-prettier`: Runs Prettier as an ESLint rule.
|
|
39
|
-
- `prettier`: Code formatter.
|
|
40
|
-
|
|
41
|
-
## Features Implemented
|
|
42
|
-
|
|
43
|
-
- A basic `add` function in `src/index.ts`.
|
|
44
|
-
- A unit test for the `add` function in `test/index.test.ts`.
|
|
45
|
-
- Linting and formatting configurations.
|
|
46
|
-
- A test script using Node.js's built-in test runner.
|
|
47
|
-
|
|
48
|
-
## Design Patterns
|
|
49
|
-
|
|
50
|
-
- **Modular Design**: The `add` function is exported from `index.ts` and imported into `index.test.ts`, demonstrating modularity.
|
|
51
|
-
- **Unit Testing**: The project includes a dedicated test file and uses a unit testing framework to verify the correctness of the `add` function.
|
package/src/cert-manager.test.ts
DELETED
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Certificate Manager
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
6
|
-
import assert from "node:assert";
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import {
|
|
11
|
-
getAllIntermediateCerts,
|
|
12
|
-
getIntermediateCerts,
|
|
13
|
-
refreshAllCerts,
|
|
14
|
-
clearCache,
|
|
15
|
-
getCacheStatus,
|
|
16
|
-
} from "./cert-manager.ts";
|
|
17
|
-
|
|
18
|
-
// Test cache directory (same as in cert-manager.ts)
|
|
19
|
-
const CERT_CACHE_DIR = path.join(os.homedir(), ".cache", "memos-mcp", "certs");
|
|
20
|
-
const CACHE_FILE = path.join(CERT_CACHE_DIR, "cert-cache.json");
|
|
21
|
-
|
|
22
|
-
describe("CertManager", () => {
|
|
23
|
-
// Store original cache if exists
|
|
24
|
-
let originalCache: string | null = null;
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
// Backup original cache if it exists (sync for beforeEach)
|
|
28
|
-
if (existsSync(CACHE_FILE)) {
|
|
29
|
-
originalCache = readFileSync(CACHE_FILE, "utf-8");
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterEach(async () => {
|
|
34
|
-
// Restore original cache
|
|
35
|
-
if (originalCache) {
|
|
36
|
-
writeFileSync(CACHE_FILE, originalCache);
|
|
37
|
-
} else if (existsSync(CACHE_FILE)) {
|
|
38
|
-
// If there was no original cache, remove the test one
|
|
39
|
-
unlinkSync(CACHE_FILE);
|
|
40
|
-
}
|
|
41
|
-
originalCache = null;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("clearCache", () => {
|
|
45
|
-
it("should remove the cache file", async () => {
|
|
46
|
-
// Ensure cache exists first
|
|
47
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
48
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
49
|
-
}
|
|
50
|
-
writeFileSync(CACHE_FILE, JSON.stringify({ version: 1, certs: {} }));
|
|
51
|
-
|
|
52
|
-
await clearCache();
|
|
53
|
-
|
|
54
|
-
assert.strictEqual(existsSync(CACHE_FILE), false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("should not throw if cache file does not exist", async () => {
|
|
58
|
-
// Ensure cache doesn't exist
|
|
59
|
-
if (existsSync(CACHE_FILE)) {
|
|
60
|
-
unlinkSync(CACHE_FILE);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
await assert.doesNotReject(async () => await clearCache());
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("getCacheStatus", () => {
|
|
68
|
-
it("should return empty object when cache is empty", async () => {
|
|
69
|
-
await clearCache();
|
|
70
|
-
|
|
71
|
-
const status = await getCacheStatus();
|
|
72
|
-
|
|
73
|
-
assert.deepStrictEqual(status, {});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("should return status for cached certificates", async () => {
|
|
77
|
-
// Clear in-memory cache first so it reloads from file
|
|
78
|
-
await clearCache();
|
|
79
|
-
|
|
80
|
-
// Create a mock cache
|
|
81
|
-
const mockCache = {
|
|
82
|
-
version: 1,
|
|
83
|
-
certs: {
|
|
84
|
-
E8: {
|
|
85
|
-
pem: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
|
|
86
|
-
fetchedAt: Date.now(),
|
|
87
|
-
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000, // 1 year from now
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
93
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
94
|
-
}
|
|
95
|
-
writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
|
|
96
|
-
|
|
97
|
-
const status = await getCacheStatus();
|
|
98
|
-
|
|
99
|
-
assert.ok(status.E8);
|
|
100
|
-
assert.ok(status.E8.fetchedAt instanceof Date);
|
|
101
|
-
assert.ok(status.E8.expiresAt instanceof Date);
|
|
102
|
-
assert.strictEqual(typeof status.E8.needsRefresh, "boolean");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("should indicate needsRefresh for old cache entries", async () => {
|
|
106
|
-
// Clear in-memory cache first so it reloads from file
|
|
107
|
-
await clearCache();
|
|
108
|
-
|
|
109
|
-
// Create an old cache entry (40 days ago)
|
|
110
|
-
const fortyDaysAgo = Date.now() - 40 * 24 * 60 * 60 * 1000;
|
|
111
|
-
const mockCache = {
|
|
112
|
-
version: 1,
|
|
113
|
-
certs: {
|
|
114
|
-
E8: {
|
|
115
|
-
pem: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
|
|
116
|
-
fetchedAt: fortyDaysAgo,
|
|
117
|
-
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
123
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
124
|
-
}
|
|
125
|
-
writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
|
|
126
|
-
|
|
127
|
-
const status = await getCacheStatus();
|
|
128
|
-
|
|
129
|
-
assert.strictEqual(status.E8.needsRefresh, true);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("should indicate needsRefresh for soon-to-expire certificates", async () => {
|
|
133
|
-
// Clear in-memory cache first so it reloads from file
|
|
134
|
-
await clearCache();
|
|
135
|
-
|
|
136
|
-
// Create a cache entry that expires in 20 days
|
|
137
|
-
const twentyDaysFromNow = Date.now() + 20 * 24 * 60 * 60 * 1000;
|
|
138
|
-
const mockCache = {
|
|
139
|
-
version: 1,
|
|
140
|
-
certs: {
|
|
141
|
-
E8: {
|
|
142
|
-
pem: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
|
|
143
|
-
fetchedAt: Date.now(),
|
|
144
|
-
expiresAt: twentyDaysFromNow,
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
150
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
151
|
-
}
|
|
152
|
-
writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
|
|
153
|
-
|
|
154
|
-
const status = await getCacheStatus();
|
|
155
|
-
|
|
156
|
-
assert.strictEqual(status.E8.needsRefresh, true);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe("getIntermediateCerts", () => {
|
|
161
|
-
it("should return empty array for unknown certificate names", async () => {
|
|
162
|
-
const result = await getIntermediateCerts(["UNKNOWN_CERT"]);
|
|
163
|
-
|
|
164
|
-
assert.deepStrictEqual(result, []);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("should return cached certificate if available and fresh", async () => {
|
|
168
|
-
// Clear in-memory cache first so it reloads from file
|
|
169
|
-
await clearCache();
|
|
170
|
-
|
|
171
|
-
// Create a fresh cache
|
|
172
|
-
const mockCache = {
|
|
173
|
-
version: 1,
|
|
174
|
-
certs: {
|
|
175
|
-
E8: {
|
|
176
|
-
pem: "-----BEGIN CERTIFICATE-----\nMOCK_E8_CERT\n-----END CERTIFICATE-----",
|
|
177
|
-
fetchedAt: Date.now(),
|
|
178
|
-
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
184
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
185
|
-
}
|
|
186
|
-
writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
|
|
187
|
-
|
|
188
|
-
const result = await getIntermediateCerts(["E8"]);
|
|
189
|
-
|
|
190
|
-
assert.ok(result.length === 1);
|
|
191
|
-
assert.ok(result[0].includes("MOCK_E8_CERT"));
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("should return multiple certificates when requested", async () => {
|
|
195
|
-
// Clear in-memory cache first so it reloads from file
|
|
196
|
-
await clearCache();
|
|
197
|
-
|
|
198
|
-
// Create cache with multiple certs
|
|
199
|
-
const mockCache = {
|
|
200
|
-
version: 1,
|
|
201
|
-
certs: {
|
|
202
|
-
E8: {
|
|
203
|
-
pem: "-----BEGIN CERTIFICATE-----\nMOCK_E8\n-----END CERTIFICATE-----",
|
|
204
|
-
fetchedAt: Date.now(),
|
|
205
|
-
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
206
|
-
},
|
|
207
|
-
R3: {
|
|
208
|
-
pem: "-----BEGIN CERTIFICATE-----\nMOCK_R3\n-----END CERTIFICATE-----",
|
|
209
|
-
fetchedAt: Date.now(),
|
|
210
|
-
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
216
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
217
|
-
}
|
|
218
|
-
writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
|
|
219
|
-
|
|
220
|
-
const result = await getIntermediateCerts(["E8", "R3"]);
|
|
221
|
-
|
|
222
|
-
assert.strictEqual(result.length, 2);
|
|
223
|
-
assert.ok(result.some((c) => c.includes("MOCK_E8")));
|
|
224
|
-
assert.ok(result.some((c) => c.includes("MOCK_R3")));
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe("getAllIntermediateCerts (integration)", () => {
|
|
229
|
-
it("should fetch and cache certificates from Let's Encrypt", async () => {
|
|
230
|
-
// Clear cache first
|
|
231
|
-
await clearCache();
|
|
232
|
-
|
|
233
|
-
// This is an integration test - it actually fetches from the network
|
|
234
|
-
const certs = await getAllIntermediateCerts();
|
|
235
|
-
|
|
236
|
-
// Should have fetched at least some certificates
|
|
237
|
-
assert.ok(certs.length > 0, "Should have fetched certificates");
|
|
238
|
-
assert.ok(
|
|
239
|
-
certs.some((c) => c.includes("-----BEGIN CERTIFICATE-----")),
|
|
240
|
-
"Should contain PEM certificates",
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Check cache was populated
|
|
244
|
-
const status = await getCacheStatus();
|
|
245
|
-
const cachedCerts = Object.keys(status);
|
|
246
|
-
assert.ok(cachedCerts.length > 0, "Should have cached certificates");
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe("refreshAllCerts (integration)", () => {
|
|
251
|
-
it("should refresh all certificates", async () => {
|
|
252
|
-
// Clear cache first
|
|
253
|
-
await clearCache();
|
|
254
|
-
|
|
255
|
-
// Refresh all certs
|
|
256
|
-
await refreshAllCerts();
|
|
257
|
-
|
|
258
|
-
// Check cache was populated
|
|
259
|
-
const status = await getCacheStatus();
|
|
260
|
-
const cachedCerts = Object.keys(status);
|
|
261
|
-
|
|
262
|
-
// Should have cached multiple certificates
|
|
263
|
-
assert.ok(cachedCerts.length >= 5, `Expected at least 5 certs, got ${cachedCerts.length}`);
|
|
264
|
-
|
|
265
|
-
// All should have been recently fetched
|
|
266
|
-
const now = Date.now();
|
|
267
|
-
for (const [name, info] of Object.entries(status)) {
|
|
268
|
-
const fetchedAge = now - info.fetchedAt.getTime();
|
|
269
|
-
// Should have been fetched within the last minute
|
|
270
|
-
assert.ok(fetchedAge < 60000, `${name} should have been fetched recently, age: ${fetchedAge}ms`);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
describe("cache persistence", () => {
|
|
276
|
-
it("should persist cache across calls", async () => {
|
|
277
|
-
// Clear in-memory cache first so it reloads from file
|
|
278
|
-
await clearCache();
|
|
279
|
-
|
|
280
|
-
// Create initial cache
|
|
281
|
-
const mockCache = {
|
|
282
|
-
version: 1,
|
|
283
|
-
certs: {
|
|
284
|
-
TEST: {
|
|
285
|
-
pem: "-----BEGIN CERTIFICATE-----\nTEST_PERSIST\n-----END CERTIFICATE-----",
|
|
286
|
-
fetchedAt: Date.now(),
|
|
287
|
-
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
293
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
294
|
-
}
|
|
295
|
-
writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
|
|
296
|
-
|
|
297
|
-
// Read status - this loads the cache
|
|
298
|
-
const status1 = await getCacheStatus();
|
|
299
|
-
assert.ok(status1.TEST);
|
|
300
|
-
|
|
301
|
-
// Read again - should still have the same data
|
|
302
|
-
const status2 = await getCacheStatus();
|
|
303
|
-
assert.ok(status2.TEST);
|
|
304
|
-
assert.strictEqual(status1.TEST.fetchedAt.getTime(), status2.TEST.fetchedAt.getTime());
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("should handle corrupted cache gracefully", async () => {
|
|
308
|
-
// Clear in-memory cache first so it reloads from file
|
|
309
|
-
await clearCache();
|
|
310
|
-
|
|
311
|
-
// Write corrupted cache
|
|
312
|
-
if (!existsSync(CERT_CACHE_DIR)) {
|
|
313
|
-
mkdirSync(CERT_CACHE_DIR, { recursive: true });
|
|
314
|
-
}
|
|
315
|
-
writeFileSync(CACHE_FILE, "not valid json {{{");
|
|
316
|
-
|
|
317
|
-
// Should not throw, should return empty
|
|
318
|
-
const status = await getCacheStatus();
|
|
319
|
-
assert.deepStrictEqual(status, {});
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
});
|
package/src/memos-client.test.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Memos API Client
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, mock, beforeEach } from "node:test";
|
|
6
|
-
import assert from "node:assert";
|
|
7
|
-
import { MemosClient } from "./memos-client.ts";
|
|
8
|
-
|
|
9
|
-
// Mock fetch function - cast to any since undici's fetch has slightly different types
|
|
10
|
-
const mockFetch = mock.fn<typeof globalThis.fetch>();
|
|
11
|
-
|
|
12
|
-
describe("MemosClient", () => {
|
|
13
|
-
let client: MemosClient;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
mockFetch.mock.resetCalls();
|
|
17
|
-
client = new MemosClient({
|
|
18
|
-
baseUrl: "https://memos.example.com",
|
|
19
|
-
accessToken: "test-token",
|
|
20
|
-
// Inject mock fetch via config instead of globalThis
|
|
21
|
-
fetch: mockFetch as unknown as typeof import("undici").fetch,
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("constructor", () => {
|
|
26
|
-
it("should remove trailing slash from baseUrl", () => {
|
|
27
|
-
const clientWithSlash = new MemosClient({
|
|
28
|
-
baseUrl: "https://memos.example.com/",
|
|
29
|
-
accessToken: "test-token",
|
|
30
|
-
fetch: mockFetch as unknown as typeof import("undici").fetch,
|
|
31
|
-
});
|
|
32
|
-
// We can verify this by checking a request URL
|
|
33
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
34
|
-
Promise.resolve(new Response(JSON.stringify({ name: "users/1" }), { status: 200 })),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
clientWithSlash.getCurrentUser();
|
|
38
|
-
|
|
39
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
40
|
-
assert.ok(callArgs);
|
|
41
|
-
assert.strictEqual(callArgs.arguments[0], "https://memos.example.com/api/v1/auth/me");
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("createMemo", () => {
|
|
46
|
-
it("should create a memo with content only", async () => {
|
|
47
|
-
const mockMemo = {
|
|
48
|
-
name: "memos/abc123",
|
|
49
|
-
content: "Test memo",
|
|
50
|
-
visibility: "PRIVATE",
|
|
51
|
-
tags: [],
|
|
52
|
-
createTime: "2024-01-01T00:00:00Z",
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
56
|
-
Promise.resolve(new Response(JSON.stringify(mockMemo), { status: 200 })),
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const result = await client.createMemo({ content: "Test memo" });
|
|
60
|
-
|
|
61
|
-
assert.strictEqual(result.name, "memos/abc123");
|
|
62
|
-
assert.strictEqual(result.content, "Test memo");
|
|
63
|
-
|
|
64
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
65
|
-
assert.ok(callArgs);
|
|
66
|
-
assert.strictEqual(callArgs.arguments[0], "https://memos.example.com/api/v1/memos");
|
|
67
|
-
assert.strictEqual(callArgs.arguments[1]?.method, "POST");
|
|
68
|
-
assert.strictEqual(
|
|
69
|
-
(callArgs.arguments[1]?.headers as Record<string, string>)?.Authorization,
|
|
70
|
-
"Bearer test-token",
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("should create a memo with visibility and location", async () => {
|
|
75
|
-
const mockMemo = {
|
|
76
|
-
name: "memos/xyz789",
|
|
77
|
-
content: "Geo memo",
|
|
78
|
-
visibility: "PUBLIC",
|
|
79
|
-
location: { placeholder: "NYC", latitude: 40.7, longitude: -74.0 },
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
83
|
-
Promise.resolve(new Response(JSON.stringify(mockMemo), { status: 200 })),
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const result = await client.createMemo({
|
|
87
|
-
content: "Geo memo",
|
|
88
|
-
visibility: "PUBLIC",
|
|
89
|
-
location: { placeholder: "NYC", latitude: 40.7, longitude: -74.0 },
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
assert.strictEqual(result.visibility, "PUBLIC");
|
|
93
|
-
assert.deepStrictEqual(result.location, { placeholder: "NYC", latitude: 40.7, longitude: -74.0 });
|
|
94
|
-
|
|
95
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
96
|
-
const body = JSON.parse(callArgs?.arguments[1]?.body as string);
|
|
97
|
-
assert.strictEqual(body.content, "Geo memo");
|
|
98
|
-
assert.strictEqual(body.visibility, "PUBLIC");
|
|
99
|
-
assert.deepStrictEqual(body.location, { placeholder: "NYC", latitude: 40.7, longitude: -74.0 });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("should throw error on API failure", async () => {
|
|
103
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
104
|
-
Promise.resolve(new Response("Unauthorized", { status: 401, statusText: "Unauthorized" })),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
await assert.rejects(async () => await client.createMemo({ content: "Test" }), {
|
|
108
|
-
message: /Memos API error: 401/,
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe("listMemos", () => {
|
|
114
|
-
it("should list memos without filters", async () => {
|
|
115
|
-
const mockResponse = {
|
|
116
|
-
memos: [{ name: "memos/1", content: "Memo 1" }],
|
|
117
|
-
nextPageToken: "token123",
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
121
|
-
Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const result = await client.listMemos();
|
|
125
|
-
|
|
126
|
-
assert.strictEqual(result.memos.length, 1);
|
|
127
|
-
assert.strictEqual(result.nextPageToken, "token123");
|
|
128
|
-
|
|
129
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
130
|
-
assert.strictEqual(callArgs?.arguments[0], "https://memos.example.com/api/v1/memos");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("should list memos with all filters", async () => {
|
|
134
|
-
const mockResponse = { memos: [], nextPageToken: undefined };
|
|
135
|
-
|
|
136
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
137
|
-
Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
await client.listMemos({
|
|
141
|
-
filter: 'visibility == "PUBLIC"',
|
|
142
|
-
pageSize: 50,
|
|
143
|
-
pageToken: "abc",
|
|
144
|
-
state: "ARCHIVED",
|
|
145
|
-
orderBy: "create_time desc",
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
149
|
-
const url = callArgs?.arguments[0] as string;
|
|
150
|
-
|
|
151
|
-
assert.ok(url.includes("filter=visibility"));
|
|
152
|
-
assert.ok(url.includes("pageSize=50"));
|
|
153
|
-
assert.ok(url.includes("pageToken=abc"));
|
|
154
|
-
assert.ok(url.includes("state=ARCHIVED"));
|
|
155
|
-
assert.ok(url.includes("orderBy=create_time"));
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe("getMemo", () => {
|
|
160
|
-
it("should get a memo by ID", async () => {
|
|
161
|
-
const mockMemo = {
|
|
162
|
-
name: "memos/abc123",
|
|
163
|
-
content: "My memo",
|
|
164
|
-
visibility: "PRIVATE",
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
168
|
-
Promise.resolve(new Response(JSON.stringify(mockMemo), { status: 200 })),
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const result = await client.getMemo("abc123");
|
|
172
|
-
|
|
173
|
-
assert.strictEqual(result.name, "memos/abc123");
|
|
174
|
-
assert.strictEqual(result.content, "My memo");
|
|
175
|
-
|
|
176
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
177
|
-
assert.strictEqual(callArgs?.arguments[0], "https://memos.example.com/api/v1/memos/abc123");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("should throw error when memo not found", async () => {
|
|
181
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
182
|
-
Promise.resolve(new Response("Not Found", { status: 404, statusText: "Not Found" })),
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
await assert.rejects(async () => await client.getMemo("nonexistent"), {
|
|
186
|
-
message: /Memos API error: 404/,
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe("searchMemos", () => {
|
|
192
|
-
it("should search memos by query", async () => {
|
|
193
|
-
const mockResponse = {
|
|
194
|
-
memos: [{ name: "memos/1", content: "Hello world" }],
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
198
|
-
Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
await client.searchMemos("Hello");
|
|
202
|
-
|
|
203
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
204
|
-
const url = callArgs?.arguments[0] as string;
|
|
205
|
-
|
|
206
|
-
// URL will be encoded, check for the encoded version
|
|
207
|
-
assert.ok(url.includes("filter="));
|
|
208
|
-
assert.ok(decodeURIComponent(url).includes('content.contains("Hello")'));
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("should escape quotes in search query", async () => {
|
|
212
|
-
const mockResponse = { memos: [] };
|
|
213
|
-
|
|
214
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
215
|
-
Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
await client.searchMemos('test "quoted" text');
|
|
219
|
-
|
|
220
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
221
|
-
const url = callArgs?.arguments[0] as string;
|
|
222
|
-
|
|
223
|
-
// Check decoded URL contains escaped quotes
|
|
224
|
-
const decodedUrl = decodeURIComponent(url);
|
|
225
|
-
assert.ok(decodedUrl.includes('\\"quoted\\"'));
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("should pass additional options to listMemos", async () => {
|
|
229
|
-
const mockResponse = { memos: [] };
|
|
230
|
-
|
|
231
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
232
|
-
Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
await client.searchMemos("test", { pageSize: 10, state: "ARCHIVED" });
|
|
236
|
-
|
|
237
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
238
|
-
const url = callArgs?.arguments[0] as string;
|
|
239
|
-
|
|
240
|
-
assert.ok(url.includes("pageSize=10"));
|
|
241
|
-
assert.ok(url.includes("state=ARCHIVED"));
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("searchByTag", () => {
|
|
246
|
-
it("should search memos by tag", async () => {
|
|
247
|
-
const mockResponse = {
|
|
248
|
-
memos: [{ name: "memos/1", content: "#important task", tags: ["important"] }],
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
252
|
-
Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
await client.searchByTag("important");
|
|
256
|
-
|
|
257
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
258
|
-
const url = callArgs?.arguments[0] as string;
|
|
259
|
-
|
|
260
|
-
// Check URL contains filter parameter with tag
|
|
261
|
-
assert.ok(url.includes("filter="));
|
|
262
|
-
assert.ok(url.includes("important"));
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe("getCurrentUser", () => {
|
|
267
|
-
it("should get current user info", async () => {
|
|
268
|
-
const mockUser = { name: "users/1", username: "testuser" };
|
|
269
|
-
|
|
270
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
271
|
-
Promise.resolve(new Response(JSON.stringify(mockUser), { status: 200 })),
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
const result = await client.getCurrentUser();
|
|
275
|
-
|
|
276
|
-
assert.deepStrictEqual(result, { name: "users/1", username: "testuser" });
|
|
277
|
-
|
|
278
|
-
const callArgs = mockFetch.mock.calls[0];
|
|
279
|
-
assert.strictEqual(callArgs?.arguments[0], "https://memos.example.com/api/v1/auth/me");
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("testConnection", () => {
|
|
284
|
-
it("should return true on successful connection", async () => {
|
|
285
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
286
|
-
Promise.resolve(new Response(JSON.stringify({ name: "users/1" }), { status: 200 })),
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
const result = await client.testConnection();
|
|
290
|
-
|
|
291
|
-
assert.strictEqual(result, true);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("should return false on failed connection", async () => {
|
|
295
|
-
mockFetch.mock.mockImplementationOnce(() =>
|
|
296
|
-
Promise.resolve(new Response("Unauthorized", { status: 401, statusText: "Unauthorized" })),
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const result = await client.testConnection();
|
|
300
|
-
|
|
301
|
-
assert.strictEqual(result, false);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("should return false on network error", async () => {
|
|
305
|
-
mockFetch.mock.mockImplementationOnce(() => Promise.reject(new Error("Network error")));
|
|
306
|
-
|
|
307
|
-
const result = await client.testConnection();
|
|
308
|
-
|
|
309
|
-
assert.strictEqual(result, false);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
"noEmit": true,
|
|
12
|
-
"declaration": true,
|
|
13
|
-
"declarationMap": true,
|
|
14
|
-
"sourceMap": true,
|
|
15
|
-
"resolveJsonModule": true,
|
|
16
|
-
"isolatedModules": true,
|
|
17
|
-
"verbatimModuleSyntax": true,
|
|
18
|
-
"noUncheckedIndexedAccess": true,
|
|
19
|
-
"noImplicitOverride": true,
|
|
20
|
-
"noPropertyAccessFromIndexSignature": false,
|
|
21
|
-
"allowImportingTsExtensions": true
|
|
22
|
-
},
|
|
23
|
-
"include": ["src/**/*.ts"],
|
|
24
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
25
|
-
}
|