Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions e2e-stub/server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
const express = require('express');

const app = express();
Expand All @@ -7,6 +8,20 @@ const calls = [];

app.use(express.json());

// CORS headers for cross-origin client-side API calls.
// Must reflect the request origin (not '*') because axios sends withCredentials: true.
app.use((req, res, next) => {
const origin = req.headers.origin || 'http://nectar:8000';
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Test-Scenario, X-Forwarded-For');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});

app.get('/accounts/bootstrap', (req, res) => {
const scenario = req.headers['x-test-scenario'];

Expand Down Expand Up @@ -229,13 +244,180 @@ app.use('/link_gateway', (req, res) => {
res.status(200).json({ status: 'ok' });
});

// --- Search API ---
app.get('/search/query', (req, res) => {
const scenario = req.headers['x-test-scenario'];

calls.push({
endpoint: '/search/query',
scenario,
query: req.query,
timestamp: Date.now(),
});

console.log(`[STUB] Search called with q=${req.query.q}, scenario: ${scenario || 'default'}`);

// Return minimal search results
res.status(200).json({
responseHeader: {
status: 0,
QTime: 1,
params: { q: req.query.q || '*:*', rows: req.query.rows || '10', start: '0' },
},
response: {
numFound: 1,
start: 0,
docs: [
{
bibcode: '2024ApJ...test..001A',
title: ['Test Paper: A Study of Testing in Astrophysics'],
author: ['Author, Test A.', 'Writer, Example B.'],
aff: ['Test University', 'Example Institute'],
pubdate: '2024-01-00',
pub: 'The Astrophysical Journal',
citation_count: 5,
read_count: 42,
'[citations]': { num_citations: 5, num_references: 10 },
property: ['REFEREED', 'ARTICLE'],
abstract: 'This is a test abstract for smoke testing purposes.',
doi: ['10.1234/test.2024.001'],
keyword: ['testing', 'smoke tests', 'astrophysics'],
doctype: 'article',
identifier: ['2024ApJ...test..001A'],
id: '1',
},
],
},
});
});

// --- Stored search / vault ---
app.get('/vault/query/:qid', (req, res) => {
calls.push({ endpoint: `/vault/query/${req.params.qid}`, timestamp: Date.now() });
res.status(200).json({ qid: req.params.qid, query: 'bibcode:2024ApJ...test..001A', numfound: 1 });
});

app.post('/vault/query', (req, res) => {
calls.push({ endpoint: '/vault/query', timestamp: Date.now() });
res.status(200).json({ qid: 'test-qid-001' });
});

// --- User settings ---
app.get('/vault/user-data', (req, res) => {
calls.push({ endpoint: '/vault/user-data', timestamp: Date.now() });
res.status(200).json({});
});

// --- Notifications ---
app.get('/vault/notifications', (req, res) => {
calls.push({ endpoint: '/vault/notifications', timestamp: Date.now() });
res.status(200).json([]);
});

app.use('/vault/notification_query', (req, res) => {
calls.push({ endpoint: req.baseUrl + req.path, timestamp: Date.now() });
res.status(200).json([]);
});

// --- Libraries ---
app.get('/biblib/libraries', (req, res) => {
calls.push({ endpoint: '/biblib/libraries', timestamp: Date.now() });
res.status(200).json({ libraries: [] });
});

app.get('/biblib/libraries/:id', (req, res) => {
calls.push({ endpoint: `/biblib/libraries/${req.params.id}`, timestamp: Date.now() });
res.status(200).json({ metadata: { name: 'Test Library', id: req.params.id, num_documents: 0 }, documents: [] });
});

// --- ORCID ---
app.use('/orcid', (req, res) => {
calls.push({ endpoint: req.baseUrl + req.path, timestamp: Date.now() });
res.status(200).json({});
});

// --- Resolver/objects ---
app.use('/resolver', (req, res) => {
calls.push({ endpoint: req.baseUrl + req.path, timestamp: Date.now() });
res.status(200).json({ links: { records: [] } });
});

app.get('/objects/query', (req, res) => {
calls.push({ endpoint: '/objects/query', timestamp: Date.now() });
res.status(200).json({});
});

// --- Graphics ---
app.get('/graphics/:bibcode', (req, res) => {
calls.push({ endpoint: `/graphics/${req.params.bibcode}`, timestamp: Date.now() });
res.status(200).json({});
});

// --- Metrics ---
app.get('/metrics', (req, res) => {
calls.push({ endpoint: '/metrics', timestamp: Date.now() });
res.status(200).json({});
});

app.post('/metrics', (req, res) => {
calls.push({ endpoint: '/metrics', timestamp: Date.now() });
res.status(200).json({});
});

// --- Export ---
app.post('/export/:format', (req, res) => {
calls.push({ endpoint: `/export/${req.params.format}`, timestamp: Date.now() });
res.status(200).json({ msg: 'Retrieved 0 abstracts, took 0 seconds.', export: '' });
});

// --- Reference resolver ---
app.post('/reference/text', (req, res) => {
calls.push({ endpoint: '/reference/text', timestamp: Date.now() });
res.status(200).json({ resolved: { bibcode: [] } });
});

// --- Citation helper ---
app.post('/citation_helper', (req, res) => {
calls.push({ endpoint: '/citation_helper', timestamp: Date.now() });
res.status(200).json({ new: [], recommendations: [] });
});

// --- Author network / paper network / concept cloud ---
app.use('/vis', (req, res) => {
calls.push({ endpoint: req.baseUrl + req.path, timestamp: Date.now() });
res.status(200).json({ data: {} });
});

// --- Catch-all: return 404 for unknown endpoints so missing stubs are visible ---
app.use((req, res) => {
calls.push({
endpoint: req.path,
method: req.method,
timestamp: Date.now(),
note: 'catch-all-404',
});
console.warn(`[STUB] WARNING: Unhandled ${req.method} ${req.path}`);
res.status(404).json({ error: 'not-stubbed', path: req.path });
});

app.listen(PORT, () => {
console.log(`[STUB] E2E stub backend listening on http://127.0.0.1:${PORT}`);
console.log('[STUB] Endpoints:');
console.log(' - GET /accounts/bootstrap');
console.log(' - POST /accounts/user/login');
console.log(' - GET /accounts/verify/:token');
console.log(' - ALL /link_gateway/*');
console.log(' - GET /search/query');
console.log(' - GET /vault/query/:qid');
console.log(' - POST /vault/query');
console.log(' - GET /vault/user-data');
console.log(' - GET /vault/notifications');
console.log(' - GET /biblib/libraries');
console.log(' - GET /resolver/:bibcode/*');
console.log(' - GET /graphics/:bibcode');
console.log(' - GET /metrics');
console.log(' - POST /export/:format');
console.log(' - ALL (catch-all -> 404)');
console.log(' - GET /__test__/calls');
console.log(' - POST /__test__/reset');
});
52 changes: 52 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,55 @@ pnpm test:e2e:docker
lsof -ti:8000 | xargs kill -9
lsof -ti:18080 | xargs kill -9
```

## Page Object Model

Tests use the Page Object Model (POM) pattern. Page objects live in `e2e/pages/` and encapsulate selectors and actions for each page.

### Directory structure

```
e2e/
├── fixtures/
│ ├── nectar.fixture.ts — Playwright fixtures (provides page objects + helpers)
│ └── helpers.ts — Cookie/response utility functions
├── pages/
│ ├── base.page.ts — Abstract base class (navigation, cookies, scenarios)
│ ├── home.page.ts — Landing page
│ ├── login.page.ts — Login form page
│ ├── search.page.ts — Search results page
│ ├── register.page.ts — Registration page
│ ├── forgot-password.page.ts — Forgot password page
│ ├── verify.page.ts — Email verification page
│ ├── settings.page.ts — User settings page
│ └── index.ts — Barrel export
└── tests/
├── middleware/ — Middleware integration tests
└── smoke/ — Smoke/navigation tests
```

### Adding a new page object

1. Create `e2e/pages/my-page.page.ts` extending `BasePage`
2. Set the `path` property (e.g., `/my-page`)
3. Add selectors as private readonly properties
4. Add action methods (e.g., `fillForm`, `submit`)
5. Export from `e2e/pages/index.ts`
6. Add a fixture in `e2e/fixtures/nectar.fixture.ts`

### Using page objects in tests

```typescript
import { test, expect } from '../../fixtures/nectar.fixture';

test('example', async ({ loginPage, searchPage }) => {
await loginPage.addSessionCookie('anonymous-session');
await loginPage.setScenarioHeader('bootstrap-anonymous');
await loginPage.goto();

await loginPage.fillCredentials('[email protected]', 'pass');
await loginPage.submit();
});
```

Page objects are provided automatically via Playwright fixtures — destructure them in the test signature.
5 changes: 3 additions & 2 deletions e2e/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ services:
- NODE_TLS_REJECT_UNAUTHORIZED=0
- API_HOST_SERVER=http://stub:18080
- API_HOST_CLIENT=http://stub:18080
- NEXT_PUBLIC_API_HOST_CLIENT=http://127.0.0.1:18080
- NEXT_PUBLIC_API_HOST_CLIENT=
- BASE_URL=http://stub:18080
- E2E_API_PROXY=true
- NEXT_PUBLIC_BASE_CANONICAL_URL=http://127.0.0.1:8000
- ADS_SESSION_COOKIE_NAME=ads_session
- SCIX_SESSION_COOKIE_NAME=scix_session
- COOKIE_SECRET=test-secret-must-be-at-least-32-chars-long
- AUTH_SECRET=test-auth-secret-must-be-at-least-32-chars
- NEXT_PUBLIC_LOG_LEVEL=debug
- RATE_LIMIT_COUNT=100
- RATE_LIMIT_COUNT=10000
- RATE_LIMIT_MAX=1500
- RATE_LIMIT_TTL=60000
depends_on:
Expand Down
Loading
Loading