@symbiosis-lab/moss-plugin-matters 1.4.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/CHANGELOG.md +88 -0
- package/README.md +18 -0
- package/assets/icon.svg +1 -0
- package/assets/manifest.json +36 -0
- package/codegen.ts +26 -0
- package/e2e/moss-cli.test.ts +338 -0
- package/features/api/fetch-articles.feature +39 -0
- package/features/auth/wallet-auth.feature +27 -0
- package/features/download/retry-logic.feature +36 -0
- package/features/download/self-correcting.feature +83 -0
- package/features/download/worker-pool.feature +29 -0
- package/features/social/fetch-social-data.feature +40 -0
- package/features/steps/api.steps.ts +180 -0
- package/features/steps/download.steps.ts +423 -0
- package/features/steps/incremental-sync.steps.ts +105 -0
- package/features/steps/self-correcting.steps.ts +575 -0
- package/features/steps/social.steps.ts +257 -0
- package/features/steps/syndication.steps.ts +264 -0
- package/features/steps/wallet-auth.steps.ts +185 -0
- package/features/sync/article-sync.feature +49 -0
- package/features/sync/homepage-grid.feature +43 -0
- package/features/sync/incremental-sync.feature +28 -0
- package/features/syndication/create-draft.feature +35 -0
- package/package.json +58 -0
- package/src/__generated__/schema.graphql +4289 -0
- package/src/__generated__/types.ts +5355 -0
- package/src/__tests__/api.test.ts +678 -0
- package/src/__tests__/auth-route.test.ts +38 -0
- package/src/__tests__/auth-routing.test.ts +462 -0
- package/src/__tests__/auto-detect.test.ts +412 -0
- package/src/__tests__/binding-guard.test.ts +256 -0
- package/src/__tests__/config.test.ts +212 -0
- package/src/__tests__/converter.test.ts +289 -0
- package/src/__tests__/credential.test.ts +332 -0
- package/src/__tests__/domain.test.ts +341 -0
- package/src/__tests__/downloader.test.ts +679 -0
- package/src/__tests__/folder-detection.test.ts +289 -0
- package/src/__tests__/force-fresh-login.test.ts +236 -0
- package/src/__tests__/main.test.ts +2437 -0
- package/src/__tests__/progress.test.ts +93 -0
- package/src/__tests__/session.test.ts +375 -0
- package/src/__tests__/social-integration.test.ts +386 -0
- package/src/__tests__/social-sync-logic.test.ts +107 -0
- package/src/__tests__/social.test.ts +788 -0
- package/src/__tests__/sync.test.ts +1273 -0
- package/src/__tests__/syndication-toast-law.test.ts +649 -0
- package/src/__tests__/syndication.test.ts +125 -0
- package/src/__tests__/test-profile-escape.test.ts +209 -0
- package/src/__tests__/url-detect.test.ts +79 -0
- package/src/__tests__/utils.test.ts +226 -0
- package/src/api.ts +1366 -0
- package/src/auth-route.ts +38 -0
- package/src/config.ts +80 -0
- package/src/converter.ts +305 -0
- package/src/credential.ts +329 -0
- package/src/domain.ts +183 -0
- package/src/downloader.ts +761 -0
- package/src/main.ts +2092 -0
- package/src/progress.ts +89 -0
- package/src/queries/user.graphql +85 -0
- package/src/queries/viewer.graphql +104 -0
- package/src/social.ts +413 -0
- package/src/sync.ts +818 -0
- package/src/types.ts +477 -0
- package/src/url-detect.ts +49 -0
- package/src/utils.ts +305 -0
- package/test-fixtures/syndication-test-site/input/index.md +8 -0
- package/test-fixtures/syndication-test-site/input/posts/rich-test-article.md +90 -0
- package/test-helpers/TEST_ACCOUNT.md +151 -0
- package/test-helpers/api-client.ts +252 -0
- package/test-helpers/fixtures/articles.ts +147 -0
- package/test-helpers/wallet-auth.ts +305 -0
- package/test-setup/e2e.ts +93 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +39 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the Matters Syndicator Plugin
|
|
3
|
+
*
|
|
4
|
+
* This module wraps SDK utilities with plugin-specific functionality
|
|
5
|
+
* and provides Matters-specific helper functions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
setMessageContext,
|
|
10
|
+
sendMessage as sdkSendMessage,
|
|
11
|
+
reportError as sdkReportError,
|
|
12
|
+
fetchUrl,
|
|
13
|
+
downloadAsset as sdkDownloadAsset,
|
|
14
|
+
type PluginMessage,
|
|
15
|
+
} from "@symbiosis-lab/moss-api";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Plugin Configuration
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const PLUGIN_NAME = "matters";
|
|
22
|
+
|
|
23
|
+
// Initialize message context on load
|
|
24
|
+
setMessageContext(PLUGIN_NAME, "");
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Re-exports from SDK (with plugin context)
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set the current hook name for message routing
|
|
32
|
+
*/
|
|
33
|
+
export function setCurrentHookName(name: string): void {
|
|
34
|
+
setMessageContext(PLUGIN_NAME, name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the current hook name (for compatibility)
|
|
39
|
+
*/
|
|
40
|
+
let _currentHookName = "";
|
|
41
|
+
export function getCurrentHookName(): string {
|
|
42
|
+
return _currentHookName;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Override setCurrentHookName to also track locally
|
|
46
|
+
const originalSetCurrentHookName = setCurrentHookName;
|
|
47
|
+
export { originalSetCurrentHookName };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Send a message to moss (logs, progress, errors)
|
|
51
|
+
*/
|
|
52
|
+
export async function sendMessage(message: PluginMessage): Promise<void> {
|
|
53
|
+
await sdkSendMessage(message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Report an error to moss during hook execution
|
|
58
|
+
*/
|
|
59
|
+
export async function reportError(
|
|
60
|
+
error: string,
|
|
61
|
+
context?: string,
|
|
62
|
+
fatal = false
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
await sdkReportError(error, context, fatal);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// String Utilities
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate a URL-safe slug from text
|
|
73
|
+
* Preserves Unicode characters (CJK, Cyrillic, Arabic, etc.)
|
|
74
|
+
*/
|
|
75
|
+
export function slugify(text: string): string {
|
|
76
|
+
return text
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
|
79
|
+
.replace(/\s+/g, "-")
|
|
80
|
+
.replace(/--+/g, "-")
|
|
81
|
+
.replace(/^-+/, "")
|
|
82
|
+
.replace(/-+$/, "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Simple hash function for generating filenames
|
|
87
|
+
*/
|
|
88
|
+
export function simpleHash(str: string): string {
|
|
89
|
+
let hash = 0;
|
|
90
|
+
for (let i = 0; i < str.length; i++) {
|
|
91
|
+
const char = str.charCodeAt(i);
|
|
92
|
+
hash = (hash << 5) - hash + char;
|
|
93
|
+
hash = hash & hash;
|
|
94
|
+
}
|
|
95
|
+
return Math.abs(hash).toString(16);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Filename Utilities
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate a local filename from a URL
|
|
104
|
+
*/
|
|
105
|
+
export function generateLocalFilename(url: string): string | null {
|
|
106
|
+
try {
|
|
107
|
+
const urlObj = new URL(url);
|
|
108
|
+
const pathname = urlObj.pathname;
|
|
109
|
+
const cleanPath = pathname.replace(/\/public$/, "");
|
|
110
|
+
const segments = cleanPath.split("/").filter((s) => s.length > 0);
|
|
111
|
+
|
|
112
|
+
// Find a segment with an extension
|
|
113
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
114
|
+
const segment = segments[i];
|
|
115
|
+
const extMatch = segment.match(/\.(\w+)$/);
|
|
116
|
+
if (extMatch) {
|
|
117
|
+
const ext = extMatch[1].toLowerCase();
|
|
118
|
+
if (i > 0 && /^[a-f0-9-]{36}$/i.test(segments[i - 1])) {
|
|
119
|
+
return `${segments[i - 1]}.${ext}`;
|
|
120
|
+
}
|
|
121
|
+
return segment;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// No extension found, try to find a UUID
|
|
126
|
+
for (const segment of segments) {
|
|
127
|
+
if (/^[a-f0-9-]{36}$/i.test(segment)) {
|
|
128
|
+
return segment;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fallback: hash the URL
|
|
133
|
+
return simpleHash(url);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get file extension from Content-Type header
|
|
141
|
+
*/
|
|
142
|
+
export function getExtensionFromContentType(contentType: string): string | null {
|
|
143
|
+
const mapping: Record<string, string> = {
|
|
144
|
+
"image/jpeg": "jpg",
|
|
145
|
+
"image/jpg": "jpg",
|
|
146
|
+
"image/png": "png",
|
|
147
|
+
"image/gif": "gif",
|
|
148
|
+
"image/webp": "webp",
|
|
149
|
+
"image/svg+xml": "svg",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const [type, ext] of Object.entries(mapping)) {
|
|
153
|
+
if (contentType.includes(type)) {
|
|
154
|
+
return ext;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Async Utilities
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sleep for specified milliseconds
|
|
166
|
+
*/
|
|
167
|
+
export function sleep(ms: number): Promise<void> {
|
|
168
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// HTTP Utilities (using moss-api)
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Result from downloadAsset function
|
|
177
|
+
*/
|
|
178
|
+
export interface DownloadAssetResult {
|
|
179
|
+
status: number;
|
|
180
|
+
ok: boolean;
|
|
181
|
+
content_type: string | null;
|
|
182
|
+
bytes_written: number;
|
|
183
|
+
actual_path: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Fetch a URL using moss-api (bypasses WebKit CORS).
|
|
188
|
+
*
|
|
189
|
+
* Returns a Response-like object for compatibility with existing code.
|
|
190
|
+
*/
|
|
191
|
+
export async function fetchWithTimeout(
|
|
192
|
+
url: string,
|
|
193
|
+
timeoutMs = 30000
|
|
194
|
+
): Promise<Response> {
|
|
195
|
+
const result = await fetchUrl(url, { timeoutMs });
|
|
196
|
+
|
|
197
|
+
// Create Response-like object
|
|
198
|
+
const headers = new Headers();
|
|
199
|
+
if (result.contentType) {
|
|
200
|
+
headers.set("content-type", result.contentType);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new Response(result.body.buffer as ArrayBuffer, {
|
|
204
|
+
status: result.status,
|
|
205
|
+
headers,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Download a URL and save directly to disk using moss-api.
|
|
211
|
+
*
|
|
212
|
+
* This function downloads a file and writes it directly to disk without
|
|
213
|
+
* passing the binary data through JavaScript. This avoids event loop blocking
|
|
214
|
+
* that occurs with large files when using base64 encoding/decoding.
|
|
215
|
+
*
|
|
216
|
+
* moss handles filename derivation from the URL and adds file extension from
|
|
217
|
+
* Content-Type if the URL has no extension.
|
|
218
|
+
*
|
|
219
|
+
* @param url - URL to download
|
|
220
|
+
* @param targetDir - Target directory within project (e.g., "assets")
|
|
221
|
+
* @param timeoutMs - Optional timeout in milliseconds (defaults to 30 seconds)
|
|
222
|
+
* @returns Result with status, content-type, bytes written, and actual_path
|
|
223
|
+
*/
|
|
224
|
+
export async function downloadAsset(
|
|
225
|
+
url: string,
|
|
226
|
+
targetDir: string,
|
|
227
|
+
timeoutMs = 30000
|
|
228
|
+
): Promise<DownloadAssetResult> {
|
|
229
|
+
const result = await sdkDownloadAsset(url, targetDir, { timeoutMs });
|
|
230
|
+
|
|
231
|
+
// Map moss-api result to existing interface for backward compatibility
|
|
232
|
+
return {
|
|
233
|
+
status: result.status,
|
|
234
|
+
ok: result.ok,
|
|
235
|
+
content_type: result.contentType,
|
|
236
|
+
bytes_written: result.bytesWritten,
|
|
237
|
+
actual_path: result.actualPath,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Sync receipt formatting
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Format the article-sync outcome as a NOUN-LED receipt line for the progress
|
|
247
|
+
* surface — "12 articles already up to date", not a bare "12 unchanged" whose
|
|
248
|
+
* subject the reader has to guess. Pure + total-aware so the headline reads as
|
|
249
|
+
* one fact (how many articles, and what happened to them) rather than a string
|
|
250
|
+
* of disconnected counts.
|
|
251
|
+
*
|
|
252
|
+
* Image/link/comment outcomes are NOT folded in here — successes stay silent
|
|
253
|
+
* ("success makes no sound") and failed images ride a per-image advisory.
|
|
254
|
+
*/
|
|
255
|
+
export function formatArticleSyncSummary(counts: {
|
|
256
|
+
created: number;
|
|
257
|
+
updated: number;
|
|
258
|
+
skipped: number;
|
|
259
|
+
failed: number;
|
|
260
|
+
}): string {
|
|
261
|
+
const { created, updated, skipped, failed } = counts;
|
|
262
|
+
const synced = created + updated + skipped;
|
|
263
|
+
const s = (n: number): string => (n === 1 ? "" : "s");
|
|
264
|
+
|
|
265
|
+
if (synced === 0) {
|
|
266
|
+
return failed > 0
|
|
267
|
+
? `${failed} article${s(failed)} failed to sync`
|
|
268
|
+
: "no articles to sync";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let base: string;
|
|
272
|
+
if (created === 0 && updated === 0) {
|
|
273
|
+
// Every synced article already matched its remote — the common "nothing
|
|
274
|
+
// changed" run. This is the line the user found opaque as "5 unchanged".
|
|
275
|
+
base = `${synced} article${s(synced)} already up to date`;
|
|
276
|
+
} else {
|
|
277
|
+
const changed: string[] = [];
|
|
278
|
+
if (created > 0) changed.push(`${created} new`);
|
|
279
|
+
if (updated > 0) changed.push(`${updated} updated`);
|
|
280
|
+
if (skipped > 0) changed.push(`${skipped} unchanged`);
|
|
281
|
+
base = `${synced} article${s(synced)}: ${changed.join(", ")}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Trailing failure clause with an explicit verb so it reads as a SEPARATE
|
|
285
|
+
// cohort, not part of the synced set — "5 articles already up to date,
|
|
286
|
+
// 2 failed to sync" (the bare ", 2 failed" looked like 2 of the 5 broke).
|
|
287
|
+
return failed > 0 ? `${base}, ${failed} failed to sync` : base;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Binary Utilities
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Convert Uint8Array to base64 string in chunks to avoid stack overflow
|
|
296
|
+
*/
|
|
297
|
+
export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
298
|
+
let binary = "";
|
|
299
|
+
const chunkSize = 8192;
|
|
300
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
301
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
302
|
+
binary += String.fromCharCode.apply(null, chunk as unknown as number[]);
|
|
303
|
+
}
|
|
304
|
+
return btoa(binary);
|
|
305
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Exploring Decentralized Publishing: A Test Article"
|
|
3
|
+
uid: 6aef0cb6
|
|
4
|
+
date: 2025-02-10
|
|
5
|
+
tags:
|
|
6
|
+
- web3
|
|
7
|
+
- publishing
|
|
8
|
+
- decentralization
|
|
9
|
+
- test
|
|
10
|
+
description: A comprehensive test article for validating Matters syndication with rich markdown content including code, quotes, lists, and more.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Introduction
|
|
14
|
+
|
|
15
|
+
This article tests the full syndication pipeline from a local moss project to [Matters](https://matters.icu), a decentralized publishing platform built on IPFS. The goal is to verify that all markdown elements render correctly after conversion to HTML.
|
|
16
|
+
|
|
17
|
+
## Why Decentralized Publishing Matters
|
|
18
|
+
|
|
19
|
+
Traditional publishing platforms come with **inherent risks**: content can be censored, servers can go down, and users don't truly *own* their work. Decentralized platforms address these issues by:
|
|
20
|
+
|
|
21
|
+
1. Storing content on **distributed networks** like IPFS
|
|
22
|
+
2. Giving authors **cryptographic ownership** of their publications
|
|
23
|
+
3. Enabling **peer-to-peer** content distribution
|
|
24
|
+
4. Providing **censorship resistance** through redundancy
|
|
25
|
+
|
|
26
|
+
> "The internet was designed to be decentralized. We're simply returning to first principles."
|
|
27
|
+
> -- Tim Berners-Lee
|
|
28
|
+
|
|
29
|
+
## Technical Deep Dive
|
|
30
|
+
|
|
31
|
+
The syndication process works through a POSSE (Publish Own Site, Syndicate Elsewhere) workflow:
|
|
32
|
+
|
|
33
|
+
### Step 1: Content Processing
|
|
34
|
+
|
|
35
|
+
The plugin reads local markdown files and converts them to HTML using a custom converter. Here's a simplified example of the conversion pipeline:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
def convert_article(markdown_content: str) -> str:
|
|
39
|
+
"""Convert markdown to Matters-compatible HTML."""
|
|
40
|
+
html = markdown_to_html(markdown_content)
|
|
41
|
+
html = rewrite_image_urls(html)
|
|
42
|
+
html = add_canonical_link(html)
|
|
43
|
+
return html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Step 2: Draft Creation
|
|
47
|
+
|
|
48
|
+
After conversion, the plugin creates a draft on Matters via the GraphQL API:
|
|
49
|
+
|
|
50
|
+
```graphql
|
|
51
|
+
mutation PutDraft($input: PutDraftInput!) {
|
|
52
|
+
putDraft(input: $input) {
|
|
53
|
+
id
|
|
54
|
+
title
|
|
55
|
+
content
|
|
56
|
+
publishState
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Step 3: User Review & Publish
|
|
62
|
+
|
|
63
|
+
The draft opens in a browser window for review. The user can:
|
|
64
|
+
|
|
65
|
+
- Edit the title and summary
|
|
66
|
+
- Add or modify tags
|
|
67
|
+
- Upload a cover image
|
|
68
|
+
- Click **Publish** when ready
|
|
69
|
+
|
|
70
|
+
## Feature Checklist
|
|
71
|
+
|
|
72
|
+
Here's what we're testing with this article:
|
|
73
|
+
|
|
74
|
+
- [x] Basic paragraphs and text formatting
|
|
75
|
+
- [x] Headings (H2, H3)
|
|
76
|
+
- [x] Bold and italic text
|
|
77
|
+
- [x] Ordered and unordered lists
|
|
78
|
+
- [x] Code blocks with syntax highlighting
|
|
79
|
+
- [x] Blockquotes
|
|
80
|
+
- [x] External links
|
|
81
|
+
- [x] Horizontal rules
|
|
82
|
+
- [ ] Image embedding (requires separate test)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Conclusion
|
|
87
|
+
|
|
88
|
+
If you're reading this on Matters, the syndication was successful. The article was originally published on a local test site and automatically syndicated using the moss Matters plugin.
|
|
89
|
+
|
|
90
|
+
For more information about moss, visit the [project repository](https://github.com/nicholasgasior/gostatic).
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
> **Warning:** This file documents how to configure test credentials. NEVER commit a real wallet private key. Use a throwaway test account funded only with disposable test tokens.
|
|
2
|
+
|
|
3
|
+
# Matters Test Account Setup
|
|
4
|
+
|
|
5
|
+
This document describes how to set up a test account for E2E testing on Matters.icu (test environment).
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
The Matters plugin uses Ethereum wallet authentication for E2E tests. This allows programmatic login without requiring email verification, making it suitable for CI/CD environments.
|
|
10
|
+
|
|
11
|
+
## Environment
|
|
12
|
+
|
|
13
|
+
- **Test Server**: `https://server.matters.icu/graphql`
|
|
14
|
+
- **Test Website**: `https://matters.icu`
|
|
15
|
+
- **Authentication**: Ethereum wallet (EIP-4361 Sign-In with Ethereum)
|
|
16
|
+
|
|
17
|
+
## Setting Up a Test Account
|
|
18
|
+
|
|
19
|
+
### 1. Generate a Test Wallet
|
|
20
|
+
|
|
21
|
+
You can generate a new Ethereum wallet using ethers.js:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { Wallet } from "ethers";
|
|
25
|
+
|
|
26
|
+
const wallet = Wallet.createRandom();
|
|
27
|
+
console.log("Private Key:", wallet.privateKey);
|
|
28
|
+
console.log("Address:", wallet.address);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or using the command line with Foundry:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cast wallet new
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Store the Private Key
|
|
38
|
+
|
|
39
|
+
Add the private key to your GitHub repository secrets:
|
|
40
|
+
|
|
41
|
+
1. Go to your repository Settings → Secrets and variables → Actions
|
|
42
|
+
2. Create a new secret: `MATTERS_TEST_WALLET_PRIVATE_KEY`
|
|
43
|
+
3. Paste the private key (with or without `0x` prefix)
|
|
44
|
+
|
|
45
|
+
For local development, add to your `.env` file (never commit this!):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
MATTERS_TEST_WALLET_PRIVATE_KEY=0x...your-private-key...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. First Login Creates Account
|
|
52
|
+
|
|
53
|
+
The first time you authenticate with the wallet, Matters will automatically create a new account. Subsequent logins will use the existing account.
|
|
54
|
+
|
|
55
|
+
## Usage in Tests
|
|
56
|
+
|
|
57
|
+
### Basic Authentication
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { walletLogin, createAuthenticatedClient } from "./wallet-auth";
|
|
61
|
+
|
|
62
|
+
// Login with environment variable
|
|
63
|
+
const auth = await walletLogin();
|
|
64
|
+
console.log("Logged in as:", auth.user.userName);
|
|
65
|
+
|
|
66
|
+
// Create authenticated client
|
|
67
|
+
const query = createAuthenticatedClient(auth.token);
|
|
68
|
+
|
|
69
|
+
// Make authenticated requests
|
|
70
|
+
const result = await query(`
|
|
71
|
+
query {
|
|
72
|
+
viewer {
|
|
73
|
+
id
|
|
74
|
+
userName
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### With Custom Endpoint
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const auth = await walletLogin(
|
|
84
|
+
process.env.MATTERS_TEST_WALLET_PRIVATE_KEY,
|
|
85
|
+
"https://server.matters.icu/graphql"
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## CI Configuration
|
|
90
|
+
|
|
91
|
+
### GitHub Actions Example
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
name: E2E Tests
|
|
95
|
+
|
|
96
|
+
on: [push, pull_request]
|
|
97
|
+
|
|
98
|
+
jobs:
|
|
99
|
+
test:
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
env:
|
|
102
|
+
MATTERS_TEST_WALLET_PRIVATE_KEY: ${{ secrets.MATTERS_TEST_WALLET_PRIVATE_KEY }}
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
- uses: actions/setup-node@v4
|
|
106
|
+
with:
|
|
107
|
+
node-version: "20"
|
|
108
|
+
- run: npm ci
|
|
109
|
+
- run: npm run test:e2e
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Security Notes
|
|
113
|
+
|
|
114
|
+
1. **Never commit private keys** to the repository
|
|
115
|
+
2. Use GitHub Secrets for CI/CD
|
|
116
|
+
3. The test wallet should only hold test assets
|
|
117
|
+
4. Consider using a dedicated test wallet, not a production wallet
|
|
118
|
+
5. The test environment (matters.icu) is separate from production (matters.town)
|
|
119
|
+
|
|
120
|
+
## Troubleshooting
|
|
121
|
+
|
|
122
|
+
### "ethers.js is required"
|
|
123
|
+
|
|
124
|
+
Install ethers as a dev dependency:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install --save-dev ethers
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### "Login failed: auth returned false"
|
|
131
|
+
|
|
132
|
+
- Verify the private key is correct
|
|
133
|
+
- Check if the endpoint is reachable
|
|
134
|
+
- Ensure the signing message hasn't expired (10 minute validity)
|
|
135
|
+
|
|
136
|
+
### Rate Limiting
|
|
137
|
+
|
|
138
|
+
The Matters API has rate limits. If you see 429 errors, wait a few minutes before retrying.
|
|
139
|
+
|
|
140
|
+
## Test Data Considerations
|
|
141
|
+
|
|
142
|
+
When writing tests that create content (articles, comments, etc.):
|
|
143
|
+
|
|
144
|
+
1. Use descriptive titles with test identifiers (e.g., `[TEST] My Article`)
|
|
145
|
+
2. Clean up test data after tests when possible
|
|
146
|
+
3. Consider using unique identifiers to avoid conflicts between parallel test runs
|
|
147
|
+
|
|
148
|
+
## Related Files
|
|
149
|
+
|
|
150
|
+
- [wallet-auth.ts](./wallet-auth.ts) - Wallet authentication implementation
|
|
151
|
+
- [api-client.ts](./api-client.ts) - GraphQL client for public queries
|