Ders 25 / 34
Sahte Sunucular
Şemadan örnek yanıt üreten bir sunucunun yazılması, istemcinin ona karşı geliştirilmesi, aynı kodun gerçek sunucuya geçtiğinde çalıştığının ölçülmesi ve sahte sunucunun gizlediği dalların sayılması.
İçindekiler
Beklenti dosyaları şu ana kadar tek yönlü kullanıldı: tüketici yazdı, sağlayıcı sınadı. Aynı sözleşme ters yönde de iş görür. Yeni bir tüketici geliştirilmeye başlandığında sağlayıcı o uç noktayı henüz yazmamış olabilir; tüketicinin beklemesi ise gereksizdir, çünkü sözleşme zaten bellidir.
Sahte sunucu (mock server) sözleşmeyi çalıştırılabilir hâle getirir: tanımdaki şemadan örnek yanıtlar üretir ve gerçek sunucunun yerine geçer. Bu ders böyle bir sunucu yazar, istemciyi ona karşı geliştirir, sonra aynı kodu gerçek sunucuya bağlayarak iki şeyi ölçer — yanıtın biçimi aynı mı ve istemcinin hangi dalları hiç çalışmamış.
Şemaya Örnek Eklemek
Örnek üretmek her alan için mümkün değildir. Tip bilgisi bir dizgi üretmeye yeter ama
^U-\d{4}$ desenine uyan bir dizgi üretmeye yetmez. Bu yüzden deseni olan alanlar örnek
taşımak zorundadır; JSON Schema bunun için examples sözcüğünü tanımlar.
// tanim.mjs — odunc servisinin makine okunur tanimi (ornek degerlerle) // Deseni olan her alan bir ornek tasir; sahte sunucu bunlari kullanir. const ODUNC_ISTEK = { type: "object", required: ["uye", "kalemler"], additionalProperties: false, properties: { uye: { type: "string", pattern: "^U-\\d{4}$", examples: ["U-1001"] }, kalemler: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false, properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$", examples: ["978-0262033848"] } } } }, sube: { type: "string", enum: ["merkez", "sahil", "tepe"] }, }, }; const ODUNC_YANIT = { type: "object", required: ["id", "uye", "kalemler", "iadeTarihi", "durum"], additionalProperties: false, properties: { id: { type: "string", pattern: "^O-\\d+$", examples: ["O-1"] }, uye: { type: "string", pattern: "^U-\\d{4}$", examples: ["U-1001"] }, kalemler: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false, properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$", examples: ["978-0262033848"] } } } }, iadeTarihi: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$", examples: ["2026-04-15"] }, durum: { type: "string", enum: ["acik", "kapali"] }, }, }; const SORUN = { type: "object", required: ["type", "title", "status", "detail", "instance"], properties: { type: { type: "string", examples: ["https://ornek.kutuphane/sorunlar/ornek"] }, title: { type: "string", examples: ["Örnek sorun"] }, status: { type: "integer", examples: [0] }, detail: { type: "string", examples: ["Örnek açıklama."] }, instance: { type: "string", examples: ["ol-0001"] }, errors: { type: "array", items: { type: "object", required: ["yol", "kod"], properties: { yol: { type: "string", examples: ["/uye"] }, kod: { type: "string", examples: ["bicim"] } } } }, }, }; export const TANIM = { "POST /odunc": { istek: ODUNC_ISTEK, yanit: { 201: ODUNC_YANIT, 422: SORUN } }, "GET /odunc/{id}": { istek: null, yanit: { 200: ODUNC_YANIT, 404: SORUN } }, };
// ornek.mjs — semadan ornek deger uretir // Kural: enum varsa ilk deger, examples varsa ilk ornek, desen varsa ornek zorunlu. export function ornekUret(sema, yol = "") { if (sema.enum) return sema.enum[0]; if (sema.examples?.length) return sema.examples[0]; if (sema.pattern) throw new Error(`${yol || "/"}: deseni olan alan için examples yazılmamış`); switch (sema.type) { case "string": return "metin"; case "integer": case "number": return 0; case "boolean": return false; case "array": return sema.items ? [ornekUret(sema.items, `${yol}/0`)] : []; case "object": { const nesne = {}; for (const [ad, alt] of Object.entries(sema.properties ?? {})) nesne[ad] = ornekUret(alt, `${yol}/${ad}`); return nesne; } default: return null; } }
Desen varken örnek yoksa üretici hata verir. Bu, sözleşmeye konan yeni bir koşuldur: şema, kendisinden örnek üretilebilecek kadar eksiksiz olmalıdır.
// eksik-ornek.mjs — ornegi olmayan desenli alan sahte sunucuyu ayaga kaldirmaz import { ornekUret } from "./ornek.mjs"; const TAM = { type: "object", required: ["barkod"], properties: { barkod: { type: "string", pattern: "^BK-\\d{6}$", examples: ["BK-004312"] } } }; const EKSIK = { type: "object", required: ["barkod"], properties: { barkod: { type: "string", pattern: "^BK-\\d{6}$" } } }; console.log("örnekli şema ->", JSON.stringify(ornekUret(TAM))); try { ornekUret(EKSIK); } catch (h) { console.log("örneksiz şema ->", h.message); }
örnekli şema -> {"barkod":"BK-004312"}
örneksiz şema -> /barkod: deseni olan alan için examples yazılmamış
Sahte Sunucu
Sahte sunucu tanımdaki yolları tanır, örnekleri bir kez üretir ve döndürür. Örnekler başlangıçta üretildiği için eksik örnek sorunu sunucu ayağa kalkarken görülür, ilk istekte değil.
// sahte.mjs — tanimdan ornek yanit ureten sunucu // Kullanim: node sahte.mjs <port> [sorun] "sorun": hata yollarini da uretir import { createServer } from "node:http"; import { TANIM } from "./tanim.mjs"; import { ornekUret } from "./ornek.mjs"; const PORT = Number(process.argv[2] ?? 8439); const SORUN_URET = process.argv[3] === "sorun"; // Ornekler bir kez uretilir; deseni olup ornegi olmayan alan burada hata verir. const ORNEKLER = Object.fromEntries( Object.entries(TANIM).flatMap(([a, t]) => Object.entries(t.yanit).map(([k, s]) => [`${a} ${k}`, ornekUret(s)]))); const govdeOku = (istek) => new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); }); createServer(async (istek, yanit) => { yanit.sendDate = false; const yol = istek.url.split("?")[0]; const anahtar = istek.method === "POST" && yol === "/odunc" ? "POST /odunc" : istek.method === "GET" && /^\/odunc\/[^/]+$/.test(yol) ? "GET /odunc/{id}" : null; if (!anahtar) { yanit.writeHead(404).end(); return; } await govdeOku(istek); // Hangi durum kodunun uretilecegi: basari, ya da sorun kipinde hata kodu. const kodlar = Object.keys(TANIM[anahtar].yanit).map(Number); const basari = kodlar.find((k) => k < 400); const kod = SORUN_URET && istek.headers["ornek-sorun"] ? Number(istek.headers["ornek-sorun"]) : basari; const govde = structuredClone(ORNEKLER[`${anahtar} ${kod}`]); if (govde && kod >= 400) govde.status = kod; // ornek govdeye gercek kod yazilir yanit.writeHead(kod, { "content-type": kod >= 400 ? "application/problem+json; charset=utf-8" : "application/json; charset=utf-8" }); yanit.end(JSON.stringify(govde)); }).listen(PORT, "127.0.0.1", () => console.log(`sahte sunucu 127.0.0.1:${PORT}${SORUN_URET ? " (sorun kipi)" : ""}`));
İstemci, sunucu adresini dışarıdan alır. Sahte ve gerçek sunucu arasındaki geçiş bu tek bağımlılığın değişmesiyle olur; başka hiçbir şey değişmez.
// istemci.mjs — odunc kaydini gosteren istemci; sunucu adresi disaridan verilir export async function oduncGoster(taban, id, ekBaslik = {}) { const cevap = await fetch(`${taban}/odunc/${id}`, { headers: ekBaslik }); const govde = await cevap.json(); const alanlar = Object.keys(govde).join(","); if (cevap.status === 200) return { dal: "başarı", alanlar, satir: `${govde.id} · ${govde.uye} · ${govde.kalemler.length} kalem · ${govde.iadeTarihi} · ${govde.durum}` }; if (cevap.status === 404) return { dal: "bulunamadı", alanlar, satir: govde.detail }; if (cevap.status === 422) return { dal: "doğrulama", alanlar, satir: govde.errors.map((h) => `${h.yol}=${h.kod}`).join(", ") }; return { dal: "bilinmeyen", alanlar, satir: `beklenmeyen kod ${cevap.status}` }; }
// gercek.mjs — tanima uyan gercek sunucu import { createServer } from "node:http"; const KAYITLAR = new Map([["O-1", { id: "O-1", uye: "U-1001", kalemler: [{ isbn: "978-0201896831" }], iadeTarihi: "2026-05-02", durum: "kapali" }]]); createServer((istek, yanit) => { yanit.sendDate = false; const id = istek.url.split("?")[0].replace("/odunc/", ""); const kayit = KAYITLAR.get(id); if (kayit) { yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" }); return yanit.end(JSON.stringify(kayit)); } yanit.writeHead(404, { "content-type": "application/problem+json; charset=utf-8" }); yanit.end(JSON.stringify({ type: "https://ornek.kutuphane/sorunlar/kaynak-yok", title: "Kaynak bulunamadı", status: 404, detail: `${id} numaralı ödünç kaydı yok.`, instance: "ol-0007" })); }).listen(8440, "127.0.0.1", () => console.log("gercek sunucu 127.0.0.1:8440"));
Aynı Kod, Üç Sunucu
Aşağıdaki koşucu istemciyi verilen adrese karşı çalıştırır, dönen gövdenin alan kümesini yazar ve istemcinin kaç ayrı dalının çalıştığını sayar.
// kosu.mjs — ayni istemci kodunu verilen adrese karsi calistirir, hangi dallarin // calistigini ve yanitin bicimini bildirir. // Kullanim: node kosu.mjs <taban-adres> [sorun] import { oduncGoster } from "./istemci.mjs"; const TABAN = process.argv[2]; const SORUN = process.argv[3] === "sorun"; const DURUMLAR = SORUN ? [["var olan kayıt", "O-1", {}], ["olmayan kayıt", "O-9", { "ornek-sorun": "404" }]] : [["var olan kayıt", "O-1", {}], ["olmayan kayıt", "O-9", {}]]; const dallar = new Set(); for (const [ad, id, baslik] of DURUMLAR) { const s = await oduncGoster(TABAN, id, baslik); dallar.add(s.dal); console.log(`${ad.padEnd(16)} dal=${s.dal.padEnd(12)} ${s.satir}`); console.log(`${"".padEnd(16)} alanlar: ${s.alanlar}`); } console.log(`çalışan dal sayısı: ${dallar.size}/4 (${[...dallar].join(", ")})`);
#!/usr/bin/env bash # Ayni istemci kodu: once sahte sunucuya, sonra gercek sunucuya. node sahte.mjs 8439 & sahte=$! node gercek.mjs & gercek=$! sleep 0.6 echo "== sahte sunucu (yalniz basari ornekleri) ==" node kosu.mjs http://127.0.0.1:8439 kill "$sahte"; wait "$sahte" 2>/dev/null node sahte.mjs 8439 sorun & sahte=$! sleep 0.5 echo "== sahte sunucu (sorun kipi) ==" node kosu.mjs http://127.0.0.1:8439 sorun kill "$sahte"; wait "$sahte" 2>/dev/null echo "== gercek sunucu ==" node kosu.mjs http://127.0.0.1:8440 kill "$gercek"; wait "$gercek" 2>/dev/null
gercek sunucu 127.0.0.1:8440
sahte sunucu 127.0.0.1:8439
== sahte sunucu (yalniz basari ornekleri) ==
var olan kayıt dal=başarı O-1 · U-1001 · 1 kalem · 2026-04-15 · acik
alanlar: id,uye,kalemler,iadeTarihi,durum
olmayan kayıt dal=başarı O-1 · U-1001 · 1 kalem · 2026-04-15 · acik
alanlar: id,uye,kalemler,iadeTarihi,durum
çalışan dal sayısı: 1/4 (başarı)
sahte sunucu 127.0.0.1:8439 (sorun kipi)
== sahte sunucu (sorun kipi) ==
var olan kayıt dal=başarı O-1 · U-1001 · 1 kalem · 2026-04-15 · acik
alanlar: id,uye,kalemler,iadeTarihi,durum
olmayan kayıt dal=bulunamadı Örnek açıklama.
alanlar: type,title,status,detail,instance,errors
çalışan dal sayısı: 2/4 (başarı, bulunamadı)
== gercek sunucu ==
var olan kayıt dal=başarı O-1 · U-1001 · 1 kalem · 2026-05-02 · kapali
alanlar: id,uye,kalemler,iadeTarihi,durum
olmayan kayıt dal=bulunamadı O-9 numaralı ödünç kaydı yok.
alanlar: type,title,status,detail,instance
çalışan dal sayısı: 2/4 (başarı, bulunamadı)
Üçüncü bölüm asıl iddiayı doğruluyor: sahte sunucuya karşı geliştirilen istemci kodu, gerçek sunucuya bağlandığında değişmeden çalıştı. Başarı gövdesinin alan kümesi iki sunucuda birebir aynı; değerler farklı, biçim aynı. İstemcinin gerçek sunucuya taşınması için değişen tek şey adres oldu.
Birinci bölüm sahte sunucunun tuzağını gösteriyor. Yalnız başarı örnekleri üreten sahte sunucu, olmayan bir kaydı sorduğunuzda bile 200 döndürür. İstemcinin dört dalından yalnız biri çalışır ve geliştirici hata yollarını hiç görmeden işini bitmiş sayar. Bu, gerçek sunucuya geçişte en sık görülen kırılma nedenidir: eksik olan gövde biçimi değil, hiç yazılmamış hata dalıdır.
İkinci bölüm çözümü veriyor. Sahte sunucu hangi durum kodunu üreteceğini istekten öğrenebildiğinde, istemcinin bütün yolları geliştirme sırasında çalıştırılabilir. Çalışan dal sayısı bire karşı iki oldu ve gerçek sunucuyla aynı sayıya ulaştı. Sözleşmede tanımlı her durum kodunun sahte sunucudan istenebilmesi, sahte sunucuyu bir gösteri aracı olmaktan çıkarıp bir geliştirme aracı yapar.
Sahte Sunucunun Cömertliği
İkinci ve üçüncü bölümdeki 404 gövdelerinin alan kümeleri farklı: sahte sunucu errors
alanını da üretmiş, gerçek sunucu üretmemiş. İkisi de sözleşmeye uygundur, çünkü errors
zorunlu alanlar arasında değildir.
Buradaki risk tek yönlüdür. Sahte sunucu isteğe bağlı alanları da ürettiği için, istemci onların her zaman geleceğini varsayabilir. Gerçek sunucuya geçişte o varsayım çöker. Ters yönde bir risk yoktur: sahte sunucunun ürettiği zorunlu alanlar gerçek sunucuda da vardır.
Bu yüzden örnek üreticinin isteğe bağlı alanları üretip üretmeyeceği bir seçenek olmalıdır. İki kipte de çalıştırılan bir istemci, yalnız zorunlu alanlarla da ayakta kalabildiğini göstermiş olur — sözleşmenin gerçekten söylediği budur.
Özet
- Sahte sunucu, sözleşmeyi çalıştırılabilir hâle getirir ve tüketicinin sağlayıcıyı beklemeden geliştirmesini sağlar.
- Deseni olan alanlardan örnek üretilemez; şema, kendisinden örnek üretilebilecek kadar eksiksiz olmalıdır ve eksiklik sunucu ayağa kalkarken görülür.
- Aynı istemci kodu sahte ve gerçek sunucuda değişmeden çalışır; başarı gövdesinin alan kümesi ikisinde aynıdır, yalnız değerler farklıdır.
- Yalnız başarı örneği üreten sahte sunucu istemcinin dört dalından birini çalıştırır ve hata yollarını hiç yazılmamış bırakır.
- Sahte sunucudan istenen durum kodunun seçilebilmesi, çalışan dal sayısını gerçek sunucuyla aynı düzeye çıkarır.
- Sahte sunucu isteğe bağlı alanları da ürettiği için istemciyi onların hep geleceğine inandırabilir; üreticinin yalnız zorunlu alanlarla çalışan bir kipi de bulunmalıdır.
Sonraki Adım
Bu konu boyunca sözleşme hep aynı biçimde kuruldu: sunucu hangi kaynağı hangi alanlarla vereceğine karar verir, istemci verilenle yetinir. Kısmi yanıt ve alan seçimi bu katılığı gevşetmişti ama sınırı sunucu çiziyordu. Sorgu temelli yaklaşım bu ilişkiyi tersine çevirir: tip sistemi sunucuda tanımlanır, hangi alanların döneceğine istemci karar verir ve yanıt sorgunun biçimini alır. Sonraki konu bu yaklaşımı ayrıntısıyla kurar; ilk ders şema ve tip sistemini yazar, nesne, skaler, arayüz ve birleşim tiplerini tanımlar ve tipleri gerçekten çözümleyen küçük bir çalıştırıcı üzerinde gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.