Files
dreamdict/cmd/server/server_test.go
prosolis 704c06f41d Add regional variant tagging (us/gb) for English words
- SCOWL loader now loads american-words and british-words alongside
  english-words, tagging each with variant "us", "gb", or NULL (common)
- Words appearing in both American and British lists get NULL variant
- Add variant column to words table with schema migration
- API: /define returns variant field for English, /random and /words
  accept ?variant=us|gb filter
- Add total word count to server status line
- Fix rows.Close leak in PopulateCMUTails, add rows.Err() check
- Add tests for PopulateCMUTails, cmuTail, TotalWordCount
- Expand test seed data and assertions to cover pt-PT definitions,
  synonyms, and validity checks
- Update README with variant docs, /difficulty, /words, /rhyme
  endpoints, and current database statistics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 15:21:27 -07:00

277 lines
7.9 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"dreamdict/internal/dictionary"
_ "modernc.org/sqlite"
)
func mustExec(t *testing.T, db *sql.DB, query string, args ...any) {
t.Helper()
if _, err := db.Exec(query, args...); err != nil {
t.Fatalf("mustExec: %s: %v", query, err)
}
}
func setupTestServer(t *testing.T) (*dictionary.Dictionary, *http.ServeMux) {
t.Helper()
db, err := sql.Open("sqlite", ":memory:?_pragma=foreign_keys(on)")
if err != nil {
t.Fatal(err)
}
db.SetMaxOpenConns(1)
if err := dictionary.BootstrapSchema(db); err != nil {
t.Fatal(err)
}
// Seed data
mustExec(t, db, "INSERT INTO meta (key, value) VALUES ('schema_version', '1')")
mustExec(t, db, "INSERT INTO meta (key, value) VALUES ('imported_at', '2025-01-01T00:00:00Z')")
mustExec(t, db, "INSERT INTO words (id, word, lang, pos, frequency) VALUES (1, 'happy', 'en', 'adjective', 0)")
mustExec(t, db, "INSERT INTO words (id, word, lang, pos, frequency) VALUES (2, 'cat', 'en', 'noun', 0)")
mustExec(t, db, "INSERT INTO words (id, word, lang, pos, frequency) VALUES (3, 'chat', 'fr', 'noun', 5000)")
mustExec(t, db, "INSERT INTO definitions (word_id, pos, gloss, source, priority) VALUES (1, 'adjective', 'feeling pleasure', 'wordnet', 10)")
mustExec(t, db, "INSERT INTO definitions (word_id, pos, gloss, source, priority) VALUES (1, 'adjective', 'feeling joy', 'wiktionary', 20)")
mustExec(t, db, "INSERT INTO synonyms (word_id, synonym, source) VALUES (1, 'glad', 'wordnet')")
mustExec(t, db, "INSERT INTO translations (word_id, translation, target_lang, source) VALUES (2, 'chat', 'fr', 'wiktionary')")
dict := dictionary.NewFromDB(db)
mux := http.NewServeMux()
mux.HandleFunc("GET /valid", handleValid(dict))
mux.HandleFunc("GET /random", handleRandom(dict))
mux.HandleFunc("GET /define", handleDefine(dict))
mux.HandleFunc("GET /synonyms", handleSynonyms(dict))
mux.HandleFunc("GET /translate", handleTranslate(dict))
mux.HandleFunc("GET /words", handleWords(dict))
mux.HandleFunc("GET /health", handleHealth(dict, ":memory:"))
t.Cleanup(func() { db.Close() })
return dict, mux
}
func TestValidEndpoint(t *testing.T) {
_, mux := setupTestServer(t)
srv := httptest.NewServer(mux)
defer srv.Close()
// Valid word
resp, _ := http.Get(srv.URL + "/valid?word=happy&lang=en")
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
var result map[string]bool
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if !result["valid"] {
t.Error("expected valid=true")
}
// Invalid word
resp, _ = http.Get(srv.URL + "/valid?word=zzzzz&lang=en")
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if result["valid"] {
t.Error("expected valid=false")
}
// Missing params
resp, _ = http.Get(srv.URL + "/valid?word=happy")
if resp.StatusCode != 400 {
t.Errorf("missing lang: status = %d, want 400", resp.StatusCode)
}
resp.Body.Close()
}
func TestRandomEndpoint(t *testing.T) {
_, mux := setupTestServer(t)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, _ := http.Get(srv.URL + "/random?lang=en")
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
var result map[string]string
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if result["word"] == "" {
t.Error("expected non-empty word")
}
// No match
resp, _ = http.Get(srv.URL + "/random?lang=en&min=100")
if resp.StatusCode != 404 {
t.Errorf("no match: status = %d, want 404", resp.StatusCode)
}
resp.Body.Close()
}
func TestDefineEndpoint(t *testing.T) {
_, mux := setupTestServer(t)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, _ := http.Get(srv.URL + "/define?word=happy&lang=en")
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
var result struct {
Word string `json:"word"`
Lang string `json:"lang"`
Definitions []dictionary.Definition `json:"definitions"`
}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if len(result.Definitions) != 2 {
t.Errorf("got %d definitions, want 2", len(result.Definitions))
}
if result.Definitions[0].Priority > result.Definitions[1].Priority {
t.Error("definitions not ordered by priority")
}
// Word with no definitions
resp, _ = http.Get(srv.URL + "/define?word=cat&lang=en")
if resp.StatusCode != 200 {
t.Fatalf("no defs: status = %d, want 200", resp.StatusCode)
}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if len(result.Definitions) != 0 {
t.Errorf("got %d definitions, want 0", len(result.Definitions))
}
}
func TestSynonymsEndpoint(t *testing.T) {
_, mux := setupTestServer(t)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, _ := http.Get(srv.URL + "/synonyms?word=happy&lang=en")
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
var result struct {
Synonyms []string `json:"synonyms"`
}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if len(result.Synonyms) != 1 {
t.Errorf("got %d synonyms, want 1", len(result.Synonyms))
}
// No synonyms
resp, _ = http.Get(srv.URL + "/synonyms?word=cat&lang=en")
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if len(result.Synonyms) != 0 {
t.Errorf("got %d synonyms, want 0", len(result.Synonyms))
}
}
func TestTranslateEndpoint(t *testing.T) {
_, mux := setupTestServer(t)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, _ := http.Get(srv.URL + "/translate?word=cat&from=en&to=fr")
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
var result struct {
Translations []string `json:"translations"`
}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if len(result.Translations) != 1 || result.Translations[0] != "chat" {
t.Errorf("got %v, want [chat]", result.Translations)
}
// No translations
resp, _ = http.Get(srv.URL + "/translate?word=happy&from=en&to=fr")
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if len(result.Translations) != 0 {
t.Errorf("got %d, want 0", len(result.Translations))
}
}
func TestWordsEndpoint(t *testing.T) {
_, mux := setupTestServer(t)
srv := httptest.NewServer(mux)
defer srv.Close()
// All English words
resp, _ := http.Get(srv.URL + "/words?lang=en")
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
var result struct {
Lang string `json:"lang"`
Count int `json:"count"`
Words []string `json:"words"`
}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if result.Lang != "en" {
t.Errorf("lang = %q, want en", result.Lang)
}
if result.Count != len(result.Words) {
t.Errorf("count %d != len(words) %d", result.Count, len(result.Words))
}
if result.Count != 2 { // happy, cat
t.Errorf("count = %d, want 2", result.Count)
}
// With length filter — no match
resp, _ = http.Get(srv.URL + "/words?lang=en&min=100")
if resp.StatusCode != 404 {
t.Errorf("no match: status = %d, want 404", resp.StatusCode)
}
resp.Body.Close()
// Missing lang
resp, _ = http.Get(srv.URL + "/words")
if resp.StatusCode != 400 {
t.Errorf("missing lang: status = %d, want 400", resp.StatusCode)
}
resp.Body.Close()
// Bad lang
resp, _ = http.Get(srv.URL + "/words?lang=xx")
if resp.StatusCode != 400 {
t.Errorf("bad lang: status = %d, want 400", resp.StatusCode)
}
resp.Body.Close()
}
func TestHealthEndpoint(t *testing.T) {
_, mux := setupTestServer(t)
srv := httptest.NewServer(mux)
defer srv.Close()
resp, _ := http.Get(srv.URL + "/health")
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
if result["status"] != "ok" {
t.Errorf("status = %v, want ok", result["status"])
}
counts, ok := result["word_counts"].(map[string]any)
if !ok {
t.Fatal("word_counts not a map")
}
if counts["en"].(float64) != 2 {
t.Errorf("en word count = %v, want 2", counts["en"])
}
}