Skip to content
Merged
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
57 changes: 57 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: PR Checks

on:
pull_request:
branches: [ "main" ]

jobs:
test:
name: Run Tests
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run test suite
run: npm test

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
continue-on-error: true

docker-build:
name: Docker Build Test
runs-on: ubuntu-latest
needs: test

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image (no push)
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: scurry:pr-${{ github.event.pull_request.number }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
APP_QB_URL=http://localhost:8080
provenance: false
sbom: false
9 changes: 8 additions & 1 deletion __tests__/mam-token.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,18 @@ describe('mam-token API', () => {
json: vi.fn().mockResolvedValue({ token: validToken })
};

vi.spyOn(fs, 'existsSync').mockReturnValue(true);
// Mock existsSync to return false for directory check so mkdirSync gets called
vi.spyOn(fs, 'existsSync').mockImplementation((path) => {
// Return false for the directory so it gets created
if (path === 'secrets') return false;
// Return true for anything else (like file checks)
return true;
});
vi.spyOn(fs, 'mkdirSync').mockImplementation();
vi.spyOn(fs, 'writeFileSync').mockImplementation();

const res = await POST(mockRequest);

expect(res.status).toBe(200);

const data = await res.json();
Expand Down
16 changes: 8 additions & 8 deletions __tests__/middleware.test.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { middleware } from '../middleware.js';
import proxy from '../proxy.ts';
import { SESSION_COOKIE, ALLOWED_PATHS } from '../src/lib/constants.js';
import { NextResponse } from 'next/server';

describe('middleware', () => {
describe('proxy (middleware)', () => {
function mockRequest(pathname, cookieValue, search = '') {
// Use a real URL object for nextUrl
const url = new URL('http://localhost' + pathname + search);
Expand All @@ -27,38 +27,38 @@ describe('middleware', () => {
// For static files, use the path as is; for routes, append a subpath
const path = base.startsWith('/') && base.length > 1 && !base.includes('.') ? base + '/foo' : base;
const req = mockRequest(path);
const res = middleware(req);
const res = proxy(req);
expect(res).toEqual(NextResponse.next());
});
});

test('allows /login', () => {
const req = mockRequest('/login');
const res = middleware(req);
const res = proxy(req);
expect(res).toEqual(NextResponse.next());
});

test('redirects to /login if no session cookie', () => {
const req = mockRequest('/protected');
const res = middleware(req);
const res = proxy(req);
expect(res.headers.get('location')).toBe('http://localhost/login?redirect=%2Fprotected');
});

test('redirects to /login with query string preserved', () => {
const req = mockRequest('/', '', '?q=test+search');
const res = middleware(req);
const res = proxy(req);
expect(res.headers.get('location')).toBe('http://localhost/login?redirect=%2F%3Fq%3Dtest%2Bsearch');
});

test('redirects to /login without redirect param for root without query', () => {
const req = mockRequest('/');
const res = middleware(req);
const res = proxy(req);
expect(res.headers.get('location')).toBe('http://localhost/login');
});

test('allows if session cookie is present', () => {
const req = mockRequest('/protected', '1');
const res = middleware(req);
const res = proxy(req);
expect(res).toEqual(NextResponse.next());
});
});
4 changes: 2 additions & 2 deletions app/components/TokenManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,11 @@ export default function TokenManager({ onTokenUpdate }) {
<strong>How to get your MAM session token:</strong>
</p>
<ol className="text-sm text-blue-700 mt-1 list-decimal list-inside space-y-1">
<li>Go to your <a href="https://www.myanonamouse.net/preferences/index.php?view=security" class="underline" target="_blank" rel="noopener noreferrer">Security Preferences</a>.</li>
<li>Go to your <a href="https://www.myanonamouse.net/preferences/index.php?view=security" className="underline" target="_blank" rel="noopener noreferrer">Security Preferences</a>.</li>
<li>Create a session with the IP where you&apos;ll run Scurry.</li>
<li>Copy your session token value and paste it above.</li>
</ol>
<i class="mt-2 block text-xs text-blue-800">Note: do not prepend <code class="bg-gray-200 text-red-600 p-1 rounded">MAM_ID=</code> above - just the raw token value.</i>
<i className="mt-2 block text-xs text-blue-800">Note: do not prepend <code className="bg-gray-200 text-red-600 p-1 rounded">MAM_ID=</code> above - just the raw token value.</i>
</div>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable React Compiler for automatic memoization
reactCompiler: true,
experimental: {
// Enable Turbopack filesystem caching for faster dev startup
turbopackFileSystemCacheForDev: true,
// Server Actions configuration (still experimental in 16.1.4)
serverActions: {
allowedOrigins: ['*']
}
Expand Down
Loading