Preserve subscriptions on request failure and isolate account state
This commit is contained in:
@@ -780,3 +780,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
|
|||||||
- GET 503 → görünür hata → yeniden yükle → iki okunmamış kayıt; POST 503 → hata ve okunmamış durumların korunması; hatayı kaldır → açık okundu işlemi → sayfayı yeniden yükle → iki kalıcı okunmuş kayıt ve yinelenmeme doğrulandı.
|
- GET 503 → görünür hata → yeniden yükle → iki okunmamış kayıt; POST 503 → hata ve okunmamış durumların korunması; hatayı kaldır → açık okundu işlemi → sayfayı yeniden yükle → iki kalıcı okunmuş kayıt ve yinelenmeme doğrulandı.
|
||||||
- Kanıt: `qa-notification-acknowledgement.json`, 6 kontrol doğru. Test altyapısına yalnız localhost hata kontrol sayfası ve sentetik kayıt eklendi; üretim kodu bu adımda değişmedi, önceki 173 test/build sonucu yeniden çalıştırılmadı.
|
- Kanıt: `qa-notification-acknowledgement.json`, 6 kontrol doğru. Test altyapısına yalnız localhost hata kontrol sayfası ve sentetik kayıt eklendi; üretim kodu bu adımda değişmedi, önceki 173 test/build sonucu yeniden çalıştırılmadı.
|
||||||
- Bu kabul gerçek bildirim API/deposunu kullanır; dış e-posta, Telegram veya işletim sistemi push teslimi testi değildir. Planın diğer kabul maddeleri açık.
|
- Bu kabul gerçek bildirim API/deposunu kullanır; dış e-posta, Telegram veya işletim sistemi push teslimi testi değildir. Planın diğer kabul maddeleri açık.
|
||||||
|
|
||||||
|
### Bildirim abonelikleri hata ve hesap izolasyonu — 6 Eylül 2026
|
||||||
|
- Listeleme hatası boş abonelik listesine dönüştürülüyordu; DELETE yanıtında success kontrol edilmeden liste boşaltılıyordu. Ekran ortak süre sınırlı hesap isteğine geçirildi, yanıt liste şeması doğrulanıyor. Hatalar mevcut listeyi/formu koruyarak görünür; yeniden yükleme düğmesi var.
|
||||||
|
- Form hesap+oturum anahtarıyla ayrıldı, kapanan formun geç yanıtları atılır. Kayıt/silme/yenilemede eşzamanlı işlem kilidi var. Site taraması da süre ve hesap kontrollerine bağlandı; geçersiz sonuç render edilmez. Silme düğmesine abonelik özetiyle erişilebilir ad verildi.
|
||||||
|
- Sunucu filtre tipleri/listeleri ve DELETE kimliği doğrulanıyor; desteklenmeyen metot 405. notify.sqlite yapılandırılmış kullanıcı volume'una bağlı ve 0600 izinli; eski özel dizinli kurulum için taşıma gereksinimi runbook'ta.
|
||||||
|
- 174 test ve üretim derlemesi geçti. Yeni depo testi geçersiz kriterin kayıt oluşturmamasını, hesap izolasyonunu ve sahip silmesini doğruluyor. Bu yeni formun tarayıcı hata/kayıt/sil kabulü ve gerçek e-posta teslimi henüz yapılmadı.
|
||||||
|
- Mevcut abonelik kimliği (aynı şehir/sektör/alım türü/anahtar) yeniden kayıtta konu listesini güncelliyor; farklı konu kümelerini bağımsız abonelikler olarak tutma davranışı ayrıca ele alınmalı. D09 tamamlanmış sayılmaz.
|
||||||
|
|||||||
@@ -61,3 +61,5 @@ Kalıcı genel analiz ve teklif işleri için süreç kesintisi davranışı:
|
|||||||
- `node --test tests/ai-job-crash.test.mjs`: iki gerçek alt süreç SIGKILL testi; yeni süreçte kaynak/sonuç ve SQLite bütünlüğü kontrol edilir. Lease süresi testte saat parametresiyle ilerletilir. Gerçek AI sağlayıcısı ve üretim platformunun volume yeniden bağlama davranışı bu testin kapsamı dışındadır.
|
- `node --test tests/ai-job-crash.test.mjs`: iki gerçek alt süreç SIGKILL testi; yeni süreçte kaynak/sonuç ve SQLite bütünlüğü kontrol edilir. Lease süresi testte saat parametresiyle ilerletilir. Gerçek AI sağlayıcısı ve üretim platformunun volume yeniden bağlama davranışı bu testin kapsamı dışındadır.
|
||||||
|
|
||||||
Bildirim ve takip taraması deposu artık `IHALEIST_USER_DATA_DIR/takip.sqlite` içindedir; değişken yoksa mevcut `server/data/takip.sqlite` yolu korunur. Özel kullanıcı veri diziniyle çalışan eski kurulum yükseltilirken eski sabit yoldaki takip.sqlite ayrıca envantere alınmalıdır. Tüm yazıcılar durdurularak SQLite tutarlı yedeği doğrulanmalı ve hedefte mevcut bir takip.sqlite üzerine yazmadan yeni dizine aktarılmalıdır. Bu yerel değişiklik üretim verisini taşımadı. Tam volume yedeği bu dosyayı kapsar; yalnız `backup-user-data.mjs` kullanmak bildirimleri yedeklemez.
|
Bildirim ve takip taraması deposu artık `IHALEIST_USER_DATA_DIR/takip.sqlite` içindedir; değişken yoksa mevcut `server/data/takip.sqlite` yolu korunur. Özel kullanıcı veri diziniyle çalışan eski kurulum yükseltilirken eski sabit yoldaki takip.sqlite ayrıca envantere alınmalıdır. Tüm yazıcılar durdurularak SQLite tutarlı yedeği doğrulanmalı ve hedefte mevcut bir takip.sqlite üzerine yazmadan yeni dizine aktarılmalıdır. Bu yerel değişiklik üretim verisini taşımadı. Tam volume yedeği bu dosyayı kapsar; yalnız `backup-user-data.mjs` kullanmak bildirimleri yedeklemez.
|
||||||
|
|
||||||
|
E-posta aboneliği deposu `notify.sqlite` da `IHALEIST_USER_DATA_DIR` ayarını izler; değişken yoksa eski `server/data` yolu kullanılır. Özel dizinli eski kurulumlarda yükseltmeden önce sabit yoldaki notify.sqlite tutarlı biçimde yedeklenip yeni hedefe taşınmalıdır; mevcut hedef üzerine yazılmaz. Yerel değişiklik üretim deposunu taşımadı. Bu dosya yalnız kullanıcı/belge yedeğinin kapsamına girmez, tam volume envanterinde ayrıca bulunmalıdır.
|
||||||
|
|||||||
@@ -1416,9 +1416,11 @@ const server = http.createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
const id = Number(url.searchParams.get('id'));
|
const id = Number(url.searchParams.get('id'));
|
||||||
|
if(!Number.isSafeInteger(id)||id<1)return json(res,400,{success:false,error:'Geçersiz abonelik kimliği.'});
|
||||||
const ok = removeSub(id, user.email);
|
const ok = removeSub(id, user.email);
|
||||||
return json(res, ok ? 200 : 404, { success: ok, abonelikler: listSubs(user.email) });
|
return json(res, ok ? 200 : 404, { success: ok, abonelikler: listSubs(user.email) });
|
||||||
}
|
}
|
||||||
|
if(req.method!=='GET')return json(res,405,{success:false,error:'Desteklenmeyen metot.'});
|
||||||
return json(res, 200, { success: true, abonelikler: listSubs(user.email) });
|
return json(res, 200, { success: true, abonelikler: listSubs(user.email) });
|
||||||
}
|
}
|
||||||
// Aboneliklerin hemen işlenmesi (admin/test)
|
// Aboneliklerin hemen işlenmesi (admin/test)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const DATA_DIR = path.join(__dirname, 'data');
|
const DATA_DIR = process.env.IHALEIST_USER_DATA_DIR || path.join(__dirname, 'data');
|
||||||
const DB_PATH = path.join(DATA_DIR, 'notify.sqlite');
|
const DB_PATH = path.join(DATA_DIR, 'notify.sqlite');
|
||||||
|
|
||||||
const SCHEMA = `
|
const SCHEMA = `
|
||||||
@@ -47,9 +47,11 @@ function open({ create = true } = {}) {
|
|||||||
if (!DatabaseSync) return null;
|
if (!DatabaseSync) return null;
|
||||||
if (!fs.existsSync(DB_PATH)) {
|
if (!fs.existsSync(DB_PATH)) {
|
||||||
if (!create) return null;
|
if (!create) return null;
|
||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
fs.mkdirSync(DATA_DIR, { recursive: true, mode:0o700 });
|
||||||
}
|
}
|
||||||
const db = new DatabaseSync(DB_PATH);
|
const db = new DatabaseSync(DB_PATH);
|
||||||
|
fs.chmodSync(DB_PATH,0o600);
|
||||||
|
db.exec('PRAGMA busy_timeout=5000');
|
||||||
db.exec(SCHEMA);
|
db.exec(SCHEMA);
|
||||||
try {
|
try {
|
||||||
const k = db.prepare('PRAGMA table_info(abonelikler)').all().map((c) => c.name);
|
const k = db.prepare('PRAGMA table_info(abonelikler)').all().map((c) => c.name);
|
||||||
@@ -61,6 +63,9 @@ function open({ create = true } = {}) {
|
|||||||
const bosaNull = (v) => (v == null || v === '' ? '' : String(v).trim());
|
const bosaNull = (v) => (v == null || v === '' ? '' : String(v).trim());
|
||||||
|
|
||||||
export function subscribe(email, kriter = {}, ad = null) {
|
export function subscribe(email, kriter = {}, ad = null) {
|
||||||
|
if(!kriter||typeof kriter!=='object'||Array.isArray(kriter))throw new Error('Geçersiz bildirim kriterleri.');
|
||||||
|
for(const key of ['konular','kurumlar'])if(kriter[key]!==undefined&&(!Array.isArray(kriter[key])||kriter[key].length>100||kriter[key].some(value=>typeof value!=='string'||!value.trim()||value.length>300)))throw new Error('Geçersiz konu veya kurum listesi.');
|
||||||
|
for(const key of ['sehir','sektor','alimTuru','anahtar'])if(kriter[key]!==undefined&&(typeof kriter[key]!=='string'||kriter[key].length>300))throw new Error('Geçersiz filtre değeri.');
|
||||||
const db = open();
|
const db = open();
|
||||||
if (!db) throw new Error('Bildirim deposu kullanılamıyor');
|
if (!db) throw new Error('Bildirim deposu kullanılamıyor');
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useApp } from '../../context/AppContext';
|
import { useApp } from '../../context/AppContext';
|
||||||
import { TelegramBaglanti } from './TelegramBaglanti';
|
import { TelegramBaglanti } from './TelegramBaglanti';
|
||||||
import { BriefTercihi } from './BriefTercihi';
|
import { BriefTercihi } from './BriefTercihi';
|
||||||
import { getToken } from '../../services/auth';
|
import { getToken } from '../../services/auth';
|
||||||
|
import {accountRequest,requireArray} from '../../services/accountRequest';
|
||||||
|
|
||||||
import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles, ChevronDown, ChevronUp, X } from 'lucide-react';
|
import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||||
|
|
||||||
@@ -12,6 +13,10 @@ import { BellRing, Plus, Trash2, Loader2, AlertTriangle, Info, Globe, Sparkles,
|
|||||||
* isteğe bağlıdır (varsayılan: tüm Türkiye).
|
* isteğe bağlıdır (varsayılan: tüm Türkiye).
|
||||||
*/
|
*/
|
||||||
export const NotifySubscriptions = () => {
|
export const NotifySubscriptions = () => {
|
||||||
|
const {authUser}=useApp();
|
||||||
|
return authUser ? <SubscriptionForm key={`${authUser.email}:${getToken()}`} /> : null;
|
||||||
|
};
|
||||||
|
const SubscriptionForm = () => {
|
||||||
const { authUser, favoriteKurumlar, userProfile, sektorListesi } = useApp();
|
const { authUser, favoriteKurumlar, userProfile, sektorListesi } = useApp();
|
||||||
|
|
||||||
const [abonelikler, setAbonelikler] = useState(null);
|
const [abonelikler, setAbonelikler] = useState(null);
|
||||||
@@ -22,20 +27,22 @@ export const NotifySubscriptions = () => {
|
|||||||
const [gelismis, setGelismis] = useState(false);
|
const [gelismis, setGelismis] = useState(false);
|
||||||
const [daralt, setDaralt] = useState({ sehir: '', alimTuru: '', favoriKurumlar: false });
|
const [daralt, setDaralt] = useState({ sehir: '', alimTuru: '', favoriKurumlar: false });
|
||||||
|
|
||||||
|
const alive=useRef(true),mutation=useRef(false),scan=useRef(false);
|
||||||
|
|
||||||
// Site tarama
|
// Site tarama
|
||||||
const [site, setSite] = useState(userProfile?.website || '');
|
const [site, setSite] = useState(userProfile?.website || '');
|
||||||
const [tarama, setTarama] = useState(null);
|
const [tarama, setTarama] = useState(null);
|
||||||
const [taraniyor, setTaraniyor] = useState(false);
|
const [taraniyor, setTaraniyor] = useState(false);
|
||||||
const [taramaHata, setTaramaHata] = useState(null);
|
const [taramaHata, setTaramaHata] = useState(null);
|
||||||
|
|
||||||
const yukle = () => {
|
const yukle=async()=>{
|
||||||
if (!authUser) return;
|
if(mutation.current)return;
|
||||||
fetch('/api/notify/subscriptions', { headers: { Authorization: `Bearer ${getToken()}` } })
|
mutation.current=true;setBusy(true);setError(null);
|
||||||
.then((r) => r.json())
|
try {const data=await accountRequest('/api/notify/subscriptions');const rows=requireArray(data,'abonelikler');if(alive.current)setAbonelikler(rows);}
|
||||||
.then((d) => setAbonelikler(d.abonelikler || []))
|
catch(e){if(alive.current)setError(e.message);}
|
||||||
.catch(() => setAbonelikler([]));
|
finally {mutation.current=false;if(alive.current)setBusy(false);}
|
||||||
};
|
};
|
||||||
useEffect(yukle, [authUser]);
|
useEffect(()=>{alive.current=true;void yukle();return()=>{alive.current=false;};},[]);
|
||||||
|
|
||||||
const konuEkle = (k) => {
|
const konuEkle = (k) => {
|
||||||
const t = String(k || '').trim();
|
const t = String(k || '').trim();
|
||||||
@@ -44,56 +51,33 @@ export const NotifySubscriptions = () => {
|
|||||||
};
|
};
|
||||||
const konuCikar = (k) => setKonular(konular.filter((x) => x !== k));
|
const konuCikar = (k) => setKonular(konular.filter((x) => x !== k));
|
||||||
|
|
||||||
const siteyiTara = async () => {
|
const siteyiTara=async()=>{
|
||||||
setTaraniyor(true); setTaramaHata(null); setTarama(null);
|
if(scan.current)return;scan.current=true;setTaraniyor(true);setTaramaHata(null);setTarama(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/company/scan', {
|
const data=await accountRequest('/api/company/scan',{method:'POST',timeoutMs:120000,body:JSON.stringify({url:site})});
|
||||||
method: 'POST',
|
if(!Number.isFinite(data.karakter)||!Number.isFinite(data.tarananSayfa)||!Array.isArray(data.sonuc?.ihaleKonulari))throw new Error('Site tarama sonucu geçersiz.');
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
|
if(alive.current)setTarama(data);
|
||||||
body: JSON.stringify({ url: site }),
|
}catch(e){if(alive.current)setTaramaHata(e.message);}
|
||||||
});
|
finally {scan.current=false;if(alive.current)setTaraniyor(false);}
|
||||||
const d = await res.json();
|
|
||||||
if (!d.success) throw new Error(d.error);
|
|
||||||
setTarama(d);
|
|
||||||
} catch (e) {
|
|
||||||
setTaramaHata(e.message);
|
|
||||||
} finally { setTaraniyor(false); }
|
|
||||||
};
|
};
|
||||||
|
const kaydet=async()=>{
|
||||||
const kaydet = async () => {
|
if(mutation.current)return;
|
||||||
if (!konular.length) { setError('En az bir konu seçin.'); return; }
|
if(!konular.length){setError('En az bir konu seçin.');return;}
|
||||||
setBusy(true); setError(null);
|
mutation.current=true;setBusy(true);setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/notify/subscriptions', {
|
const data=await accountRequest('/api/notify/subscriptions',{method:'POST',body:JSON.stringify({kriter:{konular,sehir:daralt.sehir,alimTuru:daralt.alimTuru,kurumlar:daralt.favoriKurumlar?favoriteKurumlar:[]}})});
|
||||||
method: 'POST',
|
const rows=requireArray(data,'abonelikler');
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
|
if(alive.current){setAbonelikler(rows);setKonular([]);setDaralt({sehir:'',alimTuru:'',favoriKurumlar:false});}
|
||||||
body: JSON.stringify({
|
}catch(e){if(alive.current)setError(e.message);}
|
||||||
kriter: {
|
finally {mutation.current=false;if(alive.current)setBusy(false);}
|
||||||
konular,
|
|
||||||
sehir: daralt.sehir,
|
|
||||||
alimTuru: daralt.alimTuru,
|
|
||||||
kurumlar: daralt.favoriKurumlar ? favoriteKurumlar : [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const d = await res.json();
|
|
||||||
if (!d.success) throw new Error(d.error);
|
|
||||||
setAbonelikler(d.abonelikler);
|
|
||||||
setKonular([]); setDaralt({ sehir: '', alimTuru: '', favoriKurumlar: false });
|
|
||||||
} catch (e) { setError(e.message); }
|
|
||||||
finally { setBusy(false); }
|
|
||||||
};
|
};
|
||||||
|
const sil=async id=>{
|
||||||
const sil = async (id) => {
|
if(mutation.current)return;mutation.current=true;setBusy(true);setError(null);
|
||||||
setBusy(true);
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/notify/subscriptions?id=${id}`, {
|
const data=await accountRequest('/api/notify/subscriptions?'+new URLSearchParams({id}),{method:'DELETE'});
|
||||||
method: 'DELETE', headers: { Authorization: `Bearer ${getToken()}` },
|
const rows=requireArray(data,'abonelikler');if(alive.current)setAbonelikler(rows);
|
||||||
});
|
}catch(e){if(alive.current)setError(e.message);}
|
||||||
const d = await res.json();
|
finally {mutation.current=false;if(alive.current)setBusy(false);}
|
||||||
setAbonelikler(d.abonelikler || []);
|
|
||||||
} catch (e) { setError(e.message); }
|
|
||||||
finally { setBusy(false); }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ozet = (a) => [
|
const ozet = (a) => [
|
||||||
@@ -129,9 +113,9 @@ export const NotifySubscriptions = () => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Mevcut kayıtlar */}
|
{/* Mevcut kayıtlar */}
|
||||||
{abonelikler === null ? (
|
{abonelikler === null ? (!error && (
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-400"><Loader2 className="w-3.5 h-3.5 animate-spin" /> yükleniyor…</div>
|
<div className="flex items-center gap-2 text-xs text-slate-400"><Loader2 className="w-3.5 h-3.5 animate-spin" /> yükleniyor…</div>
|
||||||
) : abonelikler.length > 0 && (
|
)) : abonelikler.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{abonelikler.map((a) => (
|
{abonelikler.map((a) => (
|
||||||
<div key={a.id} className="flex items-start justify-between gap-2 bg-slate-50 border border-slate-200 rounded-lg px-3 py-2">
|
<div key={a.id} className="flex items-start justify-between gap-2 bg-slate-50 border border-slate-200 rounded-lg px-3 py-2">
|
||||||
@@ -141,7 +125,7 @@ export const NotifySubscriptions = () => {
|
|||||||
{a.sonGonderim ? `son gönderim: ${new Date(a.sonGonderim).toLocaleString('tr-TR')}` : 'ilk tur bekleniyor'}
|
{a.sonGonderim ? `son gönderim: ${new Date(a.sonGonderim).toLocaleString('tr-TR')}` : 'ilk tur bekleniyor'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => sil(a.id)} disabled={busy} className="text-slate-400 hover:text-red-500 shrink-0 mt-0.5" title="Sil">
|
<button onClick={() => sil(a.id)} disabled={busy} className="text-slate-400 hover:text-red-500 shrink-0 mt-0.5" aria-label={`Aboneliği sil: ${ozet(a)}`} title="Sil">
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,16 +234,16 @@ export const NotifySubscriptions = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={kaydet} disabled={busy || !konular.length}
|
<button onClick={kaydet} disabled={busy || abonelikler===null || !konular.length}
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-2">
|
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-40 text-white font-bold text-xs py-2.5 rounded-xl flex items-center justify-center gap-2">
|
||||||
{busy ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />}
|
{busy ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />}
|
||||||
Bildirimi Kaydet
|
Bildirimi Kaydet
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
|
<div role="alert" className="bg-red-50 border border-red-200 rounded-lg p-2.5 flex gap-2">
|
||||||
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
|
<AlertTriangle className="w-3.5 h-3.5 text-red-600 shrink-0 mt-0.5" />
|
||||||
<p className="text-[11px] text-red-800">{error}</p>
|
<p className="text-[11px] text-red-800">{error} <button disabled={busy} onClick={yukle} className="underline">Abonelikleri yeniden yükle</button></p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import test,{after} from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {mkdtempSync,rmSync,statSync} from 'node:fs';
|
||||||
|
import {tmpdir} from 'node:os';
|
||||||
|
import {join} from 'node:path';
|
||||||
|
const dir=mkdtempSync(join(tmpdir(),'ihaleist-subscriptions-'));process.env.IHALEIST_USER_DATA_DIR=dir;
|
||||||
|
const subscriptions=await import('../server/notify-store.mjs');
|
||||||
|
after(()=>rmSync(dir,{recursive:true,force:true}));
|
||||||
|
test('subscription writes validate criteria and deletion is account scoped in the configured volume',()=>{
|
||||||
|
for(const criteria of [[],{konular:'lab'},{konular:[{}]},{kurumlar:['']},{sehir:[]}])assert.throws(()=>subscriptions.subscribe('owner',criteria),/Geçersiz/);
|
||||||
|
assert.deepEqual(subscriptions.list('owner'),[]);
|
||||||
|
const rows=subscriptions.subscribe('owner',{konular:['Laboratuvar'],sehir:'Ankara'});
|
||||||
|
assert.equal(rows.length,1);assert.deepEqual(rows[0].konular,['Laboratuvar']);
|
||||||
|
assert.deepEqual(subscriptions.list('other'),[]);assert.equal(subscriptions.remove(rows[0].id,'other'),false);
|
||||||
|
assert.equal(subscriptions.list('owner').length,1);
|
||||||
|
assert.equal(statSync(join(dir,'notify.sqlite')).mode&0o777,0o600);
|
||||||
|
assert.equal(subscriptions.remove(rows[0].id,'owner'),true);assert.deepEqual(subscriptions.list('owner'),[]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user