diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..b9b204e --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,48 @@ +# Testing Strategy + +This project employs a hybrid testing strategy to ensure code quality while minimizing external dependencies and costs. + +## Test Categories + +### 1. Unit Tests (Standard) +* **Default Behavior:** By default, all tests run in "mock mode". +* **Goal:** Fast, reliable, and cost-free verification of logic. +* **Mechanism:** External services (Bible AI API, BibleGateway scraping) are mocked using function replacement (e.g., `SubmitQuery`, `GetPassageHTML`) or interface mocking. +* **Execution:** these tests are run automatically on every Pull Request (MR). + +### 2. Integration Tests (Live) +* **Conditional Behavior:** Specific tests are capable of switching to "live mode" when appropriate environment variables are detected. +* **Goal:** Verify that the application correctly interacts with real external services (Contract Testing) and that credentials/configurations are valid. +* **Execution:** These tests should be run on a scheduled basis (e.g., nightly or weekly) or manually when verifying infrastructure changes. + +## Live Tests & Configuration + +The following tests support live execution: + +### `TestSubmitQuery` +* **File:** `pkg/app/api_client_test.go` +* **Description:** Verifies connectivity to the Bible AI API. +* **Trigger:** + * `BIBLE_API_URL` is set AND + * `BIBLE_API_URL` is NOT `https://example.com` +* **Required Variables:** + * `BIBLE_API_URL`: The endpoint of the Bible AI API. + * `BIBLE_API_KEY`: A valid API key. +* **Rationale:** Ensures that the client code (request marshaling, auth headers) matches the actual API expectation and that the API is reachable. + +### `TestUserDatabaseIntegration` +* **File:** `pkg/app/database_integration_test.go` +* **Description:** Verifies Read/Write operations to Google Cloud Firestore/Datastore. +* **Trigger:** + * `GCLOUD_PROJECT_ID` is set. +* **Required Variables:** + * `GCLOUD_PROJECT_ID`: The Google Cloud Project ID. + * *Note:* Requires active Google Cloud credentials (e.g., `GOOGLE_APPLICATION_CREDENTIALS` or `gcloud auth`). +* **Rationale:** Verifies that database permissions and client initialization are correct, preventing runtime errors in production. Uses a specific test user ID (`test-integration-user-DO-NOT-DELETE`) to avoid affecting real user data. + +## Rationale for Strategy + +1. **Cost Reduction:** The Bible AI API may incur costs per call. Mocking prevents racking up bills during routine development. +2. **Speed:** Live calls are slow. Mocked tests run instantly. +3. **Reliability:** External services can be flaky. Mocked tests only fail if the code is broken. +4. **Verification:** We still need to know if the API changed or if our secrets are wrong. The conditional integration tests provide this safety net without the daily cost/latency penalty. diff --git a/pkg/app/api_client_test.go b/pkg/app/api_client_test.go index e2fac62..88fb4ef 100644 --- a/pkg/app/api_client_test.go +++ b/pkg/app/api_client_test.go @@ -1,15 +1,26 @@ package app import ( + "os" "testing" ) func TestSubmitQuery(t *testing.T) { t.Run("Success", func(t *testing.T) { - // Force cleanup of environment to ensure we test Secret Manager fallback - // This handles cases where the runner might have lingering env vars - defer SetEnv("BIBLE_API_URL", "https://example.com")() - defer SetEnv("BIBLE_API_KEY", "api_key")() + // Check if we should run integration test against real API + // If BIBLE_API_URL is set and not example.com, we assume integration test mode + realURL, hasURL := os.LookupEnv("BIBLE_API_URL") + if hasURL && realURL != "" && realURL != "https://example.com" { + t.Logf("Running integration test against real API: %s", realURL) + // Ensure we have a key + if _, hasKey := os.LookupEnv("BIBLE_API_KEY"); !hasKey { + t.Log("Warning: BIBLE_API_URL set but BIBLE_API_KEY missing. Test might fail.") + } + } else { + // Mock mode + defer SetEnv("BIBLE_API_URL", "https://example.com")() + defer SetEnv("BIBLE_API_KEY", "api_key")() + } ResetAPIConfigCache() diff --git a/pkg/app/devo_test.go b/pkg/app/devo_test.go index 0ad7fcb..e10fbad 100644 --- a/pkg/app/devo_test.go +++ b/pkg/app/devo_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "golang.org/x/net/html" + "github.com/julwrites/BotPlatform/pkg/def" "github.com/julwrites/ScriptureBot/pkg/utils" ) @@ -81,6 +83,19 @@ func TestGetDevotionalData(t *testing.T) { defer UnsetEnv("BIBLE_API_KEY")() ResetAPIConfigCache() + // Mock GetPassageHTML to prevent external calls during fallback + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` +
Genesis 1
+
+

Mock devotional content.

+
+ `) + } + var env def.SessionData env.Props = map[string]interface{}{"ResourcePath": "../../resource"} env.Res = GetDevotionalData(env, "DTMSV") diff --git a/pkg/app/passage_test.go b/pkg/app/passage_test.go index 21f7c21..e1dbc6b 100644 --- a/pkg/app/passage_test.go +++ b/pkg/app/passage_test.go @@ -1,16 +1,33 @@ package app import ( + "errors" "strings" "testing" + "golang.org/x/net/html" + "github.com/julwrites/BotPlatform/pkg/def" "github.com/julwrites/ScriptureBot/pkg/utils" ) +func mockGetPassageHTML(htmlStr string) *html.Node { + doc, _ := html.Parse(strings.NewReader(htmlStr)) + return doc +} + func TestGetReference(t *testing.T) { - doc := GetPassageHTML("gen 1", "NIV") + // Mock GetPassageHTML + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` +
Genesis 1
+ `) + } + doc := GetPassageHTML("gen 1", "NIV") ref := GetReference(doc) if ref != "Genesis 1" { @@ -19,6 +36,18 @@ func TestGetReference(t *testing.T) { } func TestGetPassage(t *testing.T) { + // Mock GetPassageHTML + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` +
+

In the beginning was the Word.

+
+ `) + } + doc := GetPassageHTML("john 8", "NIV") passage := GetPassage("John 8", doc, "NIV") @@ -100,6 +129,44 @@ func TestGetBiblePassage(t *testing.T) { t.Errorf("Expected failure message, got '%s'", env.Res.Message) } }) + + t.Run("Fallback: Scrape", func(t *testing.T) { + defer UnsetEnv("BIBLE_API_URL")() + defer UnsetEnv("BIBLE_API_KEY")() + ResetAPIConfigCache() + + // Mock GetPassageHTML for fallback + originalGetPassageHTML := GetPassageHTML + defer func() { GetPassageHTML = originalGetPassageHTML }() + + GetPassageHTML = func(ref, ver string) *html.Node { + return mockGetPassageHTML(` +
Genesis 1
+
+

In the beginning God created the heavens and the earth.

+
+ `) + } + + var env def.SessionData + env.Msg.Message = "gen 1" + var conf utils.UserConfig + conf.Version = "NIV" + env = utils.SetUserConfig(env, utils.SerializeUserConfig(conf)) + + // Override SubmitQuery to force failure + originalSubmitQuerySub := SubmitQuery + defer func() { SubmitQuery = originalSubmitQuerySub }() + SubmitQuery = func(req QueryRequest, result interface{}) error { + return errors.New("forced api error") + } + + env = GetBiblePassage(env) + + if !strings.Contains(env.Res.Message, "In the beginning") { + t.Errorf("Expected fallback passage content, got '%s'", env.Res.Message) + } + }) } func TestParsePassageFromHtml(t *testing.T) {