source: Klonkt/test/post-access.test.js@ 8b65e67

main
Last change on this file since 8b65e67 was 8b65e67, checked in by Bart <bart@…>, 3 weeks ago

Een gesloten bericht houdt zijn plek, en zijn tekst blijft thuis

Tweede stap voor de tijdlijn (Barts idee, naar bartoverkamp.nl): hele berichten
in de stroom in plaats van kaartjes. Een poort kan dan geen pagina meer zijn --
een betaald of fan-bericht moet ter plekke zeggen wat er is, met behoud van zijn
plek. Weglaten zou erger zijn: dan zien een supporter en een vreemde een andere
tijdlijn met andere buren, en klopt "vorige/volgende" voor niemand.

postEntry() is met opzet een functie en geen sjabloonconditie. Het lijf wordt
alleen GERENDERD als het ook getoond mag worden, dus bij een dichte poort reist
de tekst niet mee. Een <% if %> in een sjabloon krijgt hem wel in het model,
en dan staat er één vergeten conditie -- of één partial die net iets anders
samenstelt -- tussen de betaalmuur en de tekst erachter.

partials/post-body.ejs kiest alleen de vorm en gebruikt de woorden van de
bestaande poorten (pgate./fgate.), zodat ze niet uiteen gaan lopen na de eerste
tekstwijziging. Het ontgrendelen zelf blijft op de eigen pagina, want daar zit
de passkey-code; één knop ernaartoe is eerlijker dan een halve poort.

Vijf toetsen erbij, waaronder dat er bij een dichte poort niets van de tweede
alinea in het model zit.

Co-Authored-By: Claude Opus 5 <claude@…>

  • Property mode set to 100644
File size: 6.5 KB
RevLine 
[b1b7652]1// Wie mag wat zien van een bericht -- op ÉÉN plek.
2//
3// Het besluit zat alleen in de postroute, waar het een andere PAGINA rendert
4// (paid-gate/fan-gate). De tijdlijn gaat hele berichten tonen en heeft hetzelfde
5// besluit nodig als een stuk. Twee plekken die allebei zelf bepalen wie een
6// betaalde tekst mag lezen, is een lek dat op een gelegenheid wacht -- vandaar
7// deze module, en vandaar dat de toetsen hier scherper staan dan elders.
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13
14const dbMod = await import('../src/config/database.js');
15dbMod.initializeDatabase();
16const { postAccess, canReadBody, paidTeaser } = await import('../src/services/PostAccessService.js');
17
18const SITE = { id: 's1', slug: 'demo', owner_id: 'u1' };
19const OWNER = { id: 'u1', role: 'god' };
20const VREEMDE = null;
21const INGELOGD = { id: 'u2', role: 'user' };
22
23const post = (extra = {}) => ({
24 id: 'p1', slug: 'hallo', status: 'published', site_id: 's1', author_id: 'u1',
25 content: '<p>eerste alinea</p><p>tweede alinea</p>', ...extra,
26});
27
28test('een gewoon bericht is voor iedereen', () => {
29 assert.equal(postAccess(post(), { user: VREEMDE, site: SITE }), 'full');
30 assert.equal(canReadBody(post(), { user: VREEMDE, site: SITE }), true);
31});
32
33test('een concept is van de maker, en van niemand anders', () => {
34 const concept = post({ status: 'draft' });
35 assert.equal(postAccess(concept, { user: VREEMDE, site: SITE }), 'forbidden');
36 assert.equal(postAccess(concept, { user: INGELOGD, site: SITE }), 'forbidden');
37 assert.equal(postAccess(concept, { user: OWNER, site: SITE }), 'full', 'de eigenaar ziet zijn eigen concept');
38});
39
40test('fan-only vraagt om inloggen, niet om betalen', () => {
41 const f = post({ fan_only: 1 });
42 assert.equal(postAccess(f, { user: VREEMDE, site: SITE }), 'fan');
43 assert.equal(postAccess(f, { user: INGELOGD, site: SITE }), 'full', 'ingelogd is genoeg');
44});
45
46test('betaald blijft betaald tot er een geldig bewijs ligt', () => {
47 const b = post({ paid: 1 });
48 assert.equal(postAccess(b, { user: VREEMDE, site: SITE }), 'paid');
49 assert.equal(postAccess(b, { user: INGELOGD, site: SITE }), 'paid',
50 'inloggen koopt niets: een Klonkt-account is geen supporter');
51 assert.equal(postAccess(b, { user: VREEMDE, site: SITE, unlockedSlug: 'hallo' }), 'full');
52});
53
54test('een bewijs voor een ANDER bericht opent dit bericht niet', () => {
55 // De gevaarlijke vorm: één keer betalen en dan de hele site lezen.
56 const b = post({ paid: 1 });
57 assert.equal(postAccess(b, { user: VREEMDE, site: SITE, unlockedSlug: 'een-ander' }), 'paid');
58 assert.equal(postAccess(b, { user: VREEMDE, site: SITE, unlockedSlug: '' }), 'paid');
59 assert.equal(postAccess(b, { user: VREEMDE, site: SITE, unlockedSlug: null }), 'paid');
60});
61
62test('betaald WINT van fan-only, en die volgorde is geen detail', () => {
63 // Een bericht dat allebei is gaat open met een passkey, niet met een login.
64 // Andersom belandt een anonieme supporter op het inlogscherm en ziet hij de
65 // ontgrendelknop nooit -- de reden dat deze volgorde in de route stond.
66 const beide = post({ paid: 1, fan_only: 1 });
67 assert.equal(postAccess(beide, { user: VREEMDE, site: SITE }), 'paid');
68 assert.equal(postAccess(beide, { user: INGELOGD, site: SITE }), 'paid',
69 'ook ingelogd: de betaalmuur staat vooraan');
70
71 // TWEE POORTEN ACHTER ELKAAR, en dat is de bestaande werking -- overgenomen
72 // uit de route, niet hier bedacht. Wie betaalt maar niet ingelogd is, staat
73 // na het ontgrendelen alsnog voor de fanpoort:
74 assert.equal(postAccess(beide, { user: VREEMDE, site: SITE, unlockedSlug: 'hallo' }), 'fan');
75 // Betalen ÉN ingelogd komt er wel doorheen.
76 assert.equal(postAccess(beide, { user: INGELOGD, site: SITE, unlockedSlug: 'hallo' }), 'full');
77});
78
79test('de eigenaar loopt niet tegen zijn eigen muur op', () => {
80 assert.equal(postAccess(post({ paid: 1, fan_only: 1 }), { user: OWNER, site: SITE }), 'full');
81});
82
83test('geen bericht is geen toegang', () => {
84 assert.equal(postAccess(null, { user: OWNER, site: SITE }), 'forbidden');
85 assert.equal(postAccess(undefined, {}), 'forbidden');
86});
87
88test('de teaser lekt nooit voorbij de eerste alinea', () => {
89 // Dit is de hele belofte van een betaalmuur: wat hierna komt, komt er niet uit.
90 const t = paidTeaser(post({ paid: 1 }));
91 assert.match(t, /eerste alinea/);
92 assert.ok(!t.includes('tweede'), 'de tweede alinea blijft binnen');
93});
94
95test('een expliciete excerpt gaat voor', () => {
96 assert.equal(paidTeaser(post({ excerpt: 'zelf geschreven' })), 'zelf geschreven');
97});
[8b65e67]98
99// ── Wat de tijdlijn krijgt ────────────────────────────────────────────────
100// De tijdlijn toont hele berichten, dus een gesloten bericht moet zijn plek
101// houden ZONDER dat de tekst meereist. Dat is geen sjabloonkwestie: wat niet
102// gerenderd wordt, kan ook niet per ongeluk getoond worden.
103
104const { postEntry } = await import('../src/services/PostAccessService.js');
105const renderBody = (p) => `<div>GEHEIM: ${p.content}</div>`;
106
107test('een open bericht krijgt zijn lijf', () => {
108 const e = postEntry(post(), { user: VREEMDE, site: SITE }, { renderBody });
109 assert.equal(e.access, 'full');
110 assert.match(e.content_html, /GEHEIM/);
111 assert.equal(e.teaser, null, 'geen teaser NAAST het lijf');
112});
113
114test('een betaald bericht reist zonder tekst', () => {
115 const e = postEntry(post({ paid: 1 }), { user: VREEMDE, site: SITE }, { renderBody });
116 assert.equal(e.access, 'paid');
117 assert.equal(e.content_html, null, 'het lijf wordt niet eens gerenderd');
118 assert.match(e.teaser, /eerste alinea/);
119 assert.ok(!JSON.stringify(e).includes('tweede alinea'), 'en niets erachter lekt mee');
120});
121
122test('een fan-bericht ook niet', () => {
123 const e = postEntry(post({ fan_only: 1 }), { user: VREEMDE, site: SITE }, { renderBody });
124 assert.equal(e.access, 'fan');
125 assert.equal(e.content_html, null);
126});
127
128test('een concept levert helemaal niets op', () => {
129 const e = postEntry(post({ status: 'draft' }), { user: VREEMDE, site: SITE }, { renderBody });
130 assert.equal(e.access, 'forbidden');
131 assert.equal(e.content_html, null);
132 assert.equal(e.teaser, null, 'ook geen teaser: over een concept valt niets te zeggen');
133});
134
135test('zonder renderer nog steeds geen lijf, en geen fout', () => {
136 const e = postEntry(post(), { user: VREEMDE, site: SITE });
137 assert.equal(e.access, 'full');
138 assert.equal(e.content_html, null);
139});
Note: See TracBrowser for help on using the repository browser.