Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
1a5ef88c55
basic tests 2025-08-28 22:57:20 +02:00
cd9c7678ee
add AGENTS.md 2025-08-28 22:49:07 +02:00
f444bd792f
remove tests 2025-08-28 22:45:36 +02:00
74263b1e1c
add migration command to package.json 2025-08-28 22:43:21 +02:00
9 changed files with 75 additions and 208 deletions

10
AGENTS.md Normal file
View file

@ -0,0 +1,10 @@
# secajs
## Architecture
This repository contains a webapp. It covers the full stack which consists of:
- The client side code, in `src/front/`.
- The backend service in `src/`, except for `src/front/`.
- A Postgres database. Database connections and migrations are in `src/database/`.
- Besides, there is an admin CLI, with entrypoint in `src/cli.js` and commands in `src/commands/`.

View file

@ -1,4 +1,4 @@
FROM debian:latest
FROM debian:12
# Install dependencies
RUN apt-get update

View file

@ -20,12 +20,13 @@
"start": "node src/app.js",
"start:container": "docker compose up -d --build",
"stop:container": "docker compose down",
"migrate": "npx sequelize-cli db:migrate",
"build": "webpack",
"watch": "webpack --watch",
"cli": "node src/cli.js",
"test": "playwright test",
"lint": "eslint . --fix",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,html,ejs}\""
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,html,ejs}\"",
"test": "playwright test"
},
"keywords": [],
"author": "",

13
playwright.config.js Normal file
View file

@ -0,0 +1,13 @@
const { defineConfig } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
},
webServer: {
command: 'npm start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});

12
tests/basic.spec.js Normal file
View file

@ -0,0 +1,12 @@
const { test, expect } = require('@playwright/test');
test('app starts and public page is reachable', async ({ page }) => {
// Navigate to the root page
await page.goto('/');
// Check that the page loads (should redirect to login)
await expect(page).toHaveURL('/login');
// Verify we can see some content on the login page
await expect(page.locator('body')).toBeVisible();
});

View file

@ -1,106 +0,0 @@
const { test, expect, hardcodedSessionUuid } = require('./test-setup');
const SessionCreated = require('../src/models/SessionCreated');
const SessionRelatedToPublickey = require('../src/models/SessionRelatedToPublickey');
const NymSet = require('../src/models/NymSet');
const ContactDetailsSet = require('../src/models/ContactDetailsSet');
const OfferDetailsSet = require('../src/models/OfferDetailsSet');
test('Mock records are present', async () => {
for (const someModel of [
SessionCreated,
SessionRelatedToPublickey,
NymSet,
ContactDetailsSet,
]) {
expect(await someModel.findOne()).toBeTruthy();
}
});
test('Hardcoded session cookie is there', async ({ context }) => {
const page = await context.newPage();
const cookiesInPage = await page.context().cookies();
expect(cookiesInPage).toHaveLength(1);
expect(cookiesInPage[0].name).toBe('sessionUuid');
expect(cookiesInPage[0].value).toBe(hardcodedSessionUuid);
});
test('Offers is reachable', async ({ context }) => {
const page = await context.newPage();
await page.goto('http://localhost/offers');
const createOfferButton = page.locator('#button-start-create-offer');
await expect(createOfferButton).toBeVisible();
await expect(createOfferButton).toContainText('Crear nueva oferta');
});
test('Create an offer with a few options creates in DB', async ({
context,
}) => {
const page = await context.newPage();
await page.goto('http://localhost/offers');
await page.getByRole('button', { name: 'Crear nueva oferta' }).click();
await expect(page.locator('#close-offer-controls-area')).toBeVisible();
await page.getByRole('button', { name: 'Quiero vender Bitcoin' }).click();
await page.getByRole('button', { name: 'Quiero comprar Bitcoin' }).click();
await page.getByRole('button', { name: '+' }).click();
await page.getByRole('button', { name: '+' }).click();
await page.getByRole('button', { name: '+' }).click();
await page.getByRole('button', { name: '-' }).click();
await expect(page.locator('#premium-value')).toContainText('2%');
await page.locator('#input-eur-amount').click();
await page.locator('#input-eur-amount').press('ControlOrMeta+a');
await page.locator('#input-eur-amount').fill('50');
await expect(page.locator('#input-eur-amount')).toHaveValue('50');
await page
.getByText(
'Añade los detalles de tu oferta Quiero comprar Bitcoin Quiero vender Bitcoin'
)
.click();
await page
.getByRole('textbox', { name: '¿Dónde? Ej."Eixample", "La' })
.click();
await page
.getByRole('textbox', { name: '¿Dónde? Ej."Eixample", "La' })
.fill('En algún lugar');
await page
.getByRole('textbox', { name: '¿Cuándo? Ej."Cualquier hora' })
.click();
await page
.getByRole('textbox', { name: '¿Cuándo? Ej."Cualquier hora' })
.fill('En algún momento');
await page.locator('#onchain-checkbox').uncheck();
await expect(page.locator('#onchain-checkbox')).not.toBeChecked();
await expect(page.locator('#lightning-checkbox')).toBeChecked();
await page.locator('#my-trusted-trusted-checkbox').uncheck();
await page.locator('#all-members-checkbox').check();
await page.locator('#my-trusted-trusted-checkbox').check();
await expect(page.locator('#my-trusted-trusted-checkbox')).toBeChecked();
await page.locator('#all-members-checkbox').uncheck();
await expect(page.locator('#all-members-checkbox')).not.toBeChecked();
await page.locator('#large-bills-checkbox').check();
await expect(page.locator('#large-bills-checkbox')).toBeChecked();
await page.getByRole('button', { name: 'Publicar oferta' }).click();
await expect(page.locator('#offer-created-confirmation')).toBeInViewport();
await expect(
page.locator('#offer-created-confirmation')
).not.toBeInViewport();
await expect(page.locator('#close-offer-controls-area')).not.toBeVisible();
const createdOfferDetailsSetRecord = await OfferDetailsSet.findOne();
expect(createdOfferDetailsSetRecord.wants).toBe('BTC');
expect(createdOfferDetailsSetRecord.premium).toBe('0.02');
expect(createdOfferDetailsSetRecord.trade_amount_eur).toBe(50);
expect(createdOfferDetailsSetRecord.location_details).toBe('En algún lugar');
expect(createdOfferDetailsSetRecord.time_availability_details).toBe(
'En algún momento'
);
expect(createdOfferDetailsSetRecord.show_offer_to_trusted).toBe(true);
expect(createdOfferDetailsSetRecord.show_offer_to_trusted_trusted).toBe(true);
expect(createdOfferDetailsSetRecord.show_offer_to_all_members).toBe(false);
expect(createdOfferDetailsSetRecord.is_onchain_accepted).toBe(false);
expect(createdOfferDetailsSetRecord.is_lightning_accepted).toBe(true);
expect(createdOfferDetailsSetRecord.are_big_notes_accepted).toBe(true);
});

36
tests/invite.spec.js Normal file
View file

@ -0,0 +1,36 @@
const { test, expect } = require('@playwright/test');
const { execSync } = require('child_process');
test('can create invite with CLI and access invite page', async ({ page }) => {
// Create an invite using the CLI
const inviterNpub = 'npub1test1234567890abcdefghijklmnopqrstuvwxyz';
try {
const output = execSync(`npm run cli createAppInvite ${inviterNpub}`, {
encoding: 'utf8',
cwd: process.cwd()
});
// Extract the invite UUID from the CLI output
const match = output.match(/http:\/\/localhost\/invite\/([a-f0-9-]+)/);
if (!match) {
throw new Error('Could not extract invite UUID from CLI output');
}
const inviteUuid = match[1];
console.log(`Created invite with UUID: ${inviteUuid}`);
// Navigate to the invite page
await page.goto(`/invite/${inviteUuid}`);
// Check that the invite page loads correctly
await expect(page).toHaveTitle('Invite Details');
await expect(page.locator('h1')).toContainText('¡Has sido invitado a la seca!');
await expect(page.locator('#laseca-logo')).toBeVisible();
await expect(page.locator('#nostr-signup-button')).toBeVisible();
} catch (error) {
console.error('Error creating invite or accessing page:', error);
throw error;
}
});

View file

@ -1,27 +0,0 @@
// You can uncomment this below to open a recorder page
/*
const { chromium } = require('playwright');
test('Mock records are present', async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
await context.addCookies([
{
name: 'sessionUuid',
value: hardcodedSessionUuid,
domain: 'localhost',
path: '/',
expires: Math.floor(
new Date(new Date().setMonth(new Date().getMonth() + 1)).getTime() /
1000
), //This monster is this day next month, turned into epoch format
httpOnly: true,
secure: false,
sameSite: 'Lax',
},
]);
const page = await context.newPage();
await page.goto('http://localhost');
});
*/

View file

@ -1,72 +0,0 @@
const { test, expect } = require('@playwright/test');
const SessionCreated = require('../src/models/SessionCreated');
const SessionRelatedToPublickey = require('../src/models/SessionRelatedToPublickey');
const NymSet = require('../src/models/NymSet');
const ContactDetailsSet = require('../src/models/ContactDetailsSet');
const hardcodedSessionUuid = '0195423c-33d7-75f8-921b-a06e6d3cb8c5';
const hardcodedPublicKey =
'd3d4c49e7bdbbbf3082151add080e92f9a458d5dec993b371fe6d02cd394d57a';
test.beforeEach(async () => {
for (const someModel of [
SessionCreated,
SessionRelatedToPublickey,
NymSet,
ContactDetailsSet,
]) {
someModel.truncate();
}
const currentTimestamp = new Date();
const expiryTimestamp = new Date(currentTimestamp.getTime());
expiryTimestamp.setSeconds(expiryTimestamp.getSeconds() + 60);
await SessionCreated.create({
uuid: hardcodedSessionUuid,
created_at: currentTimestamp.toISOString(),
expires_at: expiryTimestamp.toISOString(),
});
await SessionRelatedToPublickey.create({
uuid: '0195423b-f9ae-737e-98f3-880f6563ed8a',
session_uuid: hardcodedSessionUuid,
public_key: hardcodedPublicKey,
created_at: new Date().toISOString(),
});
await NymSet.create({
uuid: '01954240-ddbb-7d01-9017-efb3e500d333',
public_key: hardcodedPublicKey,
nym: 'test_nym',
created_at: new Date().toISOString(),
});
await ContactDetailsSet.create({
uuid: '01954240-ddbb-7d01-9017-efb3e500d333',
public_key: hardcodedPublicKey,
encrypted_contact_details:
'+OD0/Y2IkJ99/E0KAJL/mp3kxQo4DFp1deSPnqiejlyGoeWzBiipemPVSTT/Jg/fCQbN9Pd/GJ6shxuwWECOVyB5PnMZOVJ1MPQ7I8A+63XZ0gKnSnJgry6F69f3MhEjH49JbeVJ37TbruFu/Woevo24VWz2gPXGBuyHLzeg1tyT9+7ZSygkcCrh+bchvymCoF1nNOm/UQKnwecH1wWzo8a+rNokazD1/3iey6iKmKewi+yGCgmljrB866akqBAl?iv=PAKhqTeBfYVX/muhM8xaEA==',
created_at: new Date().toISOString(),
});
});
test.beforeEach(async ({ context }) => {
await context.addCookies([
{
name: 'sessionUuid',
value: hardcodedSessionUuid,
domain: 'localhost',
path: '/',
expires: Math.floor(
new Date(new Date().setMonth(new Date().getMonth() + 1)).getTime() /
1000
), //This monster is this day next month, turned into epoch format
httpOnly: true,
secure: false,
sameSite: 'Lax',
},
]);
});
module.exports = { test, expect, hardcodedSessionUuid };