feat: add server discovery tests and enhance public key validation
- Introduced a new test suite for server discovery functionality, ensuring proper registration and response handling. - Enhanced public key validation logic to include detailed error messages for invalid keys. - Updated package.json with a new test command for the discovery tests. - Removed outdated Playwright CI workflow configuration.
This commit is contained in:
parent
ea172050a6
commit
8309770be5
7 changed files with 87 additions and 74 deletions
28
.github/workflows/playwright.yml
vendored
28
.github/workflows/playwright.yml
vendored
|
|
@ -1,28 +0,0 @@
|
|||
name: Playwright Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
|
@ -14,6 +14,9 @@ Your identity is `you@yourserver.com`. Your server, your data, your rules.
|
|||
## Roadmap
|
||||
|
||||
- **Phase 1** — Core federation. Two servers can follow each other, post, and see each other's posts.
|
||||
- - [X] — Two servers can follow each other, trust their keys and rotate them.
|
||||
- - [ ] — One server can create posts, have users following each other and dms (unencrypted for now) works.
|
||||
- - [ ] — Two servers can fetch posts, follows and other data from their users, including DMs.
|
||||
- **Phase 2** — Server trust scoring and a public vouch ledger.
|
||||
- **Phase 3** — Opt-in relay network for censorship resistance.
|
||||
- **Phase 4** — End-to-end encryption via TBD.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"email:dev": "cross-env NODE_ENV=development email dev --dir src/lib/mail/templates --port 3001",
|
||||
"test": "cross-env NODE_ENV=test playwright test",
|
||||
"test:key": "cross-env NODE_ENV=test playwright test tests/key.test.ts",
|
||||
"test:discover": "cross-env NODE_ENV=test playwright test tests/discover.test.ts",
|
||||
"build": "next build",
|
||||
"start": "cross-env NODE_ENV=production node src/server.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
|
|
|
|||
|
|
@ -2,75 +2,36 @@ import { defineConfig, devices } from '@playwright/test';
|
|||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
// Load .env.local so workers have DATABASE_URL, BETTER_AUTH_SECRET, etc.
|
||||
dotenv.config({ path: path.resolve(__dirname, '.env.local') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('')`. */
|
||||
baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'bun run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,14 +33,22 @@ async function upsertServer(url: string, publicKey: string) {
|
|||
}).onConflictDoNothing();
|
||||
}
|
||||
|
||||
const publicKeySchema = z.string().refine((key) => {
|
||||
const publicKeySchema = z.string().superRefine((key, ctx) => {
|
||||
let pub: forge.pki.rsa.PublicKey;
|
||||
try {
|
||||
const pub = forge.pki.publicKeyFromPem(key);
|
||||
return pub.n.bitLength() >= 4096;
|
||||
pub = forge.pki.publicKeyFromPem(key) as forge.pki.rsa.PublicKey;
|
||||
} catch {
|
||||
return false;
|
||||
ctx.addIssue({ code: "custom", message: "Public key is not a valid PEM-encoded RSA key", input: key });
|
||||
return;
|
||||
}
|
||||
}, { message: "Invalid public key" });
|
||||
if (!pub.n) {
|
||||
ctx.addIssue({ code: "custom", message: "Public key is not an RSA key", input: key });
|
||||
return;
|
||||
}
|
||||
if (pub.n.bitLength() < 2048) {
|
||||
ctx.addIssue({ code: "custom", message: `RSA key must be at least 2048 bits (got ${pub.n.bitLength()})`, input: key });
|
||||
}
|
||||
});
|
||||
|
||||
const schema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
|
|
@ -93,6 +101,8 @@ async function registerServer(validated: Extract<z.infer<typeof schema>, { metho
|
|||
return NextResponse.json({ error: "Invalid server" }, { status: 400 });
|
||||
} else if (response.publicKey !== validated.publicKey) {
|
||||
debug("REGISTER – public key mismatch: provided vs fetched");
|
||||
debug("REGISTER – provided public key: %s", validated.publicKey);
|
||||
debug("REGISTER – fetched public key: %s", response.publicKey);
|
||||
return NextResponse.json({ error: "Invalid public key" }, { status: 400 });
|
||||
}
|
||||
|
||||
|
|
|
|||
39
tests/discover.test.ts
Normal file
39
tests/discover.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import createDebug from "debug";
|
||||
import { clearServerRegistry, getServerByUrl, insertServerEcho, } from "./helpers/db";
|
||||
|
||||
const debug = createDebug("test:discover");
|
||||
|
||||
const url = "http://172.21.157.201:3001";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await clearServerRegistry()
|
||||
})
|
||||
test.afterEach(async () => {
|
||||
await clearServerRegistry()
|
||||
})
|
||||
|
||||
test("discover server", async ({ request, page }) => {
|
||||
const response = await request.post(`http://192.168.3.26:3000/discover`, {
|
||||
data: {
|
||||
method: "REGISTER",
|
||||
url: new URL(url).toString(),
|
||||
publicKey: process.env.FEDERATION_PUBLIC_KEY!,
|
||||
}
|
||||
})
|
||||
const status = response.status()
|
||||
const body = await response.json();
|
||||
debug("response status: %o", status);
|
||||
debug("response body: %o", body);
|
||||
expect(status).toBe(200)
|
||||
expect(body).toMatchObject({ message: "Server registered successfully" })
|
||||
expect(body.echo).toBeInstanceOf(Object) // We can't assert the exact object because the echo is generated by the server
|
||||
|
||||
// Insert the server echo into our database
|
||||
await insertServerEcho("http://192.168.3.26:3000", body.echo.publicKey as string);
|
||||
|
||||
// check on the database itself if the server was registered
|
||||
const server = await getServerByUrl("http://192.168.3.26:3000");
|
||||
expect(server).toBeDefined()
|
||||
expect(server?.publicKey).toBe(body.echo.publicKey as string)
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// tests/helpers/db.ts
|
||||
import db from "@/lib/db";
|
||||
import { rotateChallengeTokens, serverRegistry } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import forge from "node-forge";
|
||||
|
||||
export function generateKeypair() {
|
||||
|
|
@ -40,7 +41,33 @@ export async function seedChallenge(overrides?: Partial<typeof rotateChallengeTo
|
|||
return row
|
||||
}
|
||||
|
||||
export async function getServerByUrl(url: string) {
|
||||
return (await db.select().from(serverRegistry).where(eq(serverRegistry.url, url)))[0]
|
||||
}
|
||||
|
||||
export async function clearServerRegistry() {
|
||||
return await db.delete(serverRegistry)
|
||||
}
|
||||
|
||||
export async function clearRotateChallengeTokens() {
|
||||
return await db.delete(rotateChallengeTokens)
|
||||
}
|
||||
|
||||
export async function insertServerEcho(url: string, publicKey: string) {
|
||||
await db.insert(serverRegistry).values({
|
||||
id: crypto.randomUUID(),
|
||||
url,
|
||||
publicKey,
|
||||
lastSeen: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isHealthy: true,
|
||||
}).onConflictDoNothing()
|
||||
}
|
||||
|
||||
export async function clearTables() {
|
||||
await db.delete(rotateChallengeTokens)
|
||||
await db.delete(serverRegistry)
|
||||
return await Promise.all([
|
||||
clearRotateChallengeTokens(),
|
||||
clearServerRegistry(),
|
||||
])
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue