mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
5f77fa5cca
Trip invite links (#1143): each trip can have one rotating invite link in its Share panel. An existing, logged-in user who opens /join/<token> is added to the trip as a member; an anonymous visitor is sent to the login page and returned to the invite afterwards — there is no registration from this link. Reading, rotating or disabling the link all require the share_manage permission. Admin invite trip binding (#1402): the admin create-invite dialog can now bind a registration invite to a trip. Someone who registers via that link is auto-added to the trip as a member (password and OIDC paths), inside the same atomic step that consumes the invite. Adds a trip_invite_tokens table and a nullable invite_tokens.trip_id, a shared owner-safe/idempotent add-by-id helper, the manage + join endpoints, the JoinTripPage and Share-panel section, the admin trip picker, and the new i18n keys across every locale. Wiki updated. Discussion #1143
67 lines
2.6 KiB
TypeScript
67 lines
2.6 KiB
TypeScript
/**
|
|
* Unit tests for joinTripAsMember — TRIP-JOIN-001..004.
|
|
* The shared add-by-id helper behind trip invite links (#1143) and trip-bound
|
|
* admin invites (#1402): idempotent, owner-safe, missing-trip-safe.
|
|
*/
|
|
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
|
|
const { testDb, dbMock } = vi.hoisted(() => {
|
|
const Database = require('better-sqlite3');
|
|
const db = new Database(':memory:');
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
return { testDb: db, dbMock: { db } };
|
|
});
|
|
vi.mock('../../../src/db/database', () => dbMock);
|
|
|
|
import { createTables } from '../../../src/db/schema';
|
|
import { runMigrations } from '../../../src/db/migrations';
|
|
import { resetTestDb } from '../../helpers/test-db';
|
|
import { createUser, createTrip } from '../../helpers/factories';
|
|
import { joinTripAsMember } from '../../../src/services/tripMembership';
|
|
|
|
beforeAll(() => { createTables(testDb); runMigrations(testDb); });
|
|
beforeEach(() => resetTestDb(testDb));
|
|
afterAll(() => testDb.close());
|
|
|
|
function memberRow(tripId: number, userId: number) {
|
|
return testDb.prepare('SELECT * FROM trip_members WHERE trip_id = ? AND user_id = ?').get(tripId, userId);
|
|
}
|
|
|
|
describe('joinTripAsMember', () => {
|
|
it('TRIP-JOIN-001: adds a non-member and reports joined', () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: joiner } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
|
|
const r = joinTripAsMember(trip.id, joiner.id, null);
|
|
expect(r).toEqual({ joined: true, tripId: trip.id });
|
|
expect(memberRow(trip.id, joiner.id)).toBeTruthy();
|
|
});
|
|
|
|
it('TRIP-JOIN-002: never adds the trip owner as a member', () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
|
|
const r = joinTripAsMember(trip.id, owner.id, null);
|
|
expect(r.joined).toBe(false);
|
|
expect(memberRow(trip.id, owner.id)).toBeUndefined();
|
|
});
|
|
|
|
it('TRIP-JOIN-003: is idempotent for an existing member (no duplicate row)', () => {
|
|
const { user: owner } = createUser(testDb);
|
|
const { user: joiner } = createUser(testDb);
|
|
const trip = createTrip(testDb, owner.id);
|
|
|
|
expect(joinTripAsMember(trip.id, joiner.id, owner.id).joined).toBe(true);
|
|
expect(joinTripAsMember(trip.id, joiner.id, owner.id).joined).toBe(false);
|
|
const count = testDb.prepare('SELECT COUNT(*) as n FROM trip_members WHERE trip_id = ? AND user_id = ?').get(trip.id, joiner.id) as { n: number };
|
|
expect(count.n).toBe(1);
|
|
});
|
|
|
|
it('TRIP-JOIN-004: no-ops for a missing trip', () => {
|
|
const { user: joiner } = createUser(testDb);
|
|
const r = joinTripAsMember(999999, joiner.id, null);
|
|
expect(r.joined).toBe(false);
|
|
});
|
|
});
|