Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 15 additions & 4 deletions pkg/app/api_client_test.go
Original file line number Diff line number Diff line change
@@ -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()

Expand Down
15 changes: 15 additions & 0 deletions pkg/app/devo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"testing"
"time"

"golang.org/x/net/html"

"github.com/julwrites/BotPlatform/pkg/def"
"github.com/julwrites/ScriptureBot/pkg/utils"
)
Expand Down Expand Up @@ -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(`
<div class="bcv">Genesis 1</div>
<div class="passage-text">
<p>Mock devotional content.</p>
</div>
`)
}

var env def.SessionData
env.Props = map[string]interface{}{"ResourcePath": "../../resource"}
env.Res = GetDevotionalData(env, "DTMSV")
Expand Down
69 changes: 68 additions & 1 deletion pkg/app/passage_test.go
Original file line number Diff line number Diff line change
@@ -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(`
<div class="bcv">Genesis 1</div>
`)
}

doc := GetPassageHTML("gen 1", "NIV")
ref := GetReference(doc)

if ref != "Genesis 1" {
Expand All @@ -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(`
<div class="passage-text">
<p>In the beginning was the Word.</p>
</div>
`)
}

doc := GetPassageHTML("john 8", "NIV")

passage := GetPassage("John 8", doc, "NIV")
Expand Down Expand Up @@ -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(`
<div class="bcv">Genesis 1</div>
<div class="passage-text">
<p>In the beginning God created the heavens and the earth.</p>
</div>
`)
}

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) {
Expand Down
Loading