Skip to content
Merged
12 changes: 10 additions & 2 deletions .github/workflows/automation.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: Build Test Automation
on:
push:
branches:
- master
- staging
pull_request:
branches:
- master
Expand All @@ -19,10 +23,14 @@ jobs:
with:
persist-credentials: false

- name: gcloud Auth
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCLOUD_SA_KEY }}
project_id: ${{secrets.GCLOUD_PROJECT_ID}}

- name: Install, Build, Test 🔧 # This runs a series of commands as if building a live version of the project
env:
BIBLE_API_URL: ${{ secrets.BIBLE_API_URL }}
BIBLE_API_KEY: ${{ secrets.BIBLE_API_KEY }}
GCLOUD_PROJECT_ID: ${{ secrets.GCLOUD_PROJECT_ID }}
run: |
go mod tidy
Expand Down
25 changes: 8 additions & 17 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
name: Build, Stage and Deploy Automation
on:
push:
workflow_run:
workflows: ["Build Test Automation"]
types:
- completed
branches:
- master
jobs:
build-and-test:
build-and-deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}

steps:
- name: Install Go
Expand All @@ -18,23 +22,12 @@ jobs:
with:
persist-credentials: false

- name: Install, Build, Test 🔧 # This runs a series of commands as if building a live version of the project
run: |
go mod tidy
go test github.com/julwrites/ScriptureBot/pkg/utils \
github.com/julwrites/ScriptureBot/pkg/app \
github.com/julwrites/ScriptureBot/pkg/bot

- name: gcloud Auth
- name: gcloud Auth (Deployment)
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCLOUD_SA_KEY }}
credentials_json: ${{ secrets.GCLOUD_CICD_SA_KEY }}
project_id: ${{secrets.GCLOUD_PROJECT_ID}}

- name: Test gcloud
run: |
gcloud info

- name: Configure gcloud auth with Docker
run: |
gcloud auth configure-docker ${{ secrets.GCLOUD_REGION }}-docker.pkg.dev
Expand All @@ -55,11 +48,9 @@ jobs:
env:
GCLOUD_PROJECT_ID: ${{secrets.GCLOUD_PROJECT_ID}}
ARTIFACT_ID: ${{secrets.GCLOUD_ARTIFACT_REPOSITORY_ID}}
APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
TELEGRAM_ID: ${{secrets.TELEGRAM_ID}}
run: |
gcloud run deploy scripturebot --image ${{ secrets.GCLOUD_REGION }}-docker.pkg.dev/$GCLOUD_PROJECT_ID/$ARTIFACT_ID/root:latest --region ${{ secrets.GCLOUD_REGION }} --allow-unauthenticated
SERVICE_URL=$(gcloud run services describe scripturebot --region ${{ secrets.GCLOUD_REGION }} --format 'value(status.url)')
echo "Setting webhook for $SERVICE_URL"
go run cmd/webhook/main.go -url "$SERVICE_URL"

4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ toolchain go1.24.3
require (
cloud.google.com/go/datastore v1.20.0
cloud.google.com/go/secretmanager v1.16.0
github.com/joho/godotenv v1.5.1
github.com/julwrites/BotPlatform v0.0.0-20220206144002-60e1b8060734
golang.org/x/net v0.43.0
google.golang.org/api v0.247.0
gopkg.in/yaml.v2 v2.4.0
)

Expand All @@ -24,7 +26,6 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
Expand All @@ -37,7 +38,6 @@ require (
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.247.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
Expand Down
12 changes: 5 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ import (
"os"
"strings"

"github.com/julwrites/BotPlatform/pkg/secrets"
"github.com/julwrites/ScriptureBot/pkg/bot"
"github.com/julwrites/ScriptureBot/pkg/secrets"
)

func bothandler(res http.ResponseWriter, req *http.Request) {
secretsPath := "/go/bin/secrets.yaml"
secretsData, err := secrets.LoadSecrets(secretsPath)
secretsData, err := secrets.LoadSecrets()
if err != nil {
panic(err)
log.Fatalf("Failed to load secrets: %v", err)
}

switch strings.Trim(req.URL.EscapedPath(), "\n") {
Expand All @@ -32,10 +31,9 @@ func bothandler(res http.ResponseWriter, req *http.Request) {
}

func subscriptionhandler() {
secretsPath := "/go/bin/secrets.yaml"
secretsData, err := secrets.LoadSecrets(secretsPath)
secretsData, err := secrets.LoadSecrets()
if err != nil {
panic(err)
log.Fatalf("Failed to load secrets: %v", err)
}

bot.SubscriptionHandler(&secretsData)
Expand Down
65 changes: 16 additions & 49 deletions pkg/app/api_client_test.go
Original file line number Diff line number Diff line change
@@ -1,70 +1,37 @@
package app

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestSubmitQuery(t *testing.T) {
handler := newMockApiHandler()
ts := httptest.NewServer(handler)
defer ts.Close()

t.Run("Success", func(t *testing.T) {
defer setEnv("BIBLE_API_URL", ts.URL)()
ResetAPIConfigCache()

req := QueryRequest{Query: QueryObject{Prompt: "hello"}}
var resp OQueryResponse
err := SubmitQuery(req, &resp, "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if resp.Text != "Answer text" {
t.Errorf("Expected 'Answer text', got '%s'", resp.Text)
}
})
// Force cleanup of environment to ensure we test Secret Manager fallback
// This handles cases where the runner might have lingering env vars
defer UnsetEnv("BIBLE_API_URL")()
defer UnsetEnv("BIBLE_API_KEY")()

t.Run("API Error", func(t *testing.T) {
handler.statusCode = http.StatusInternalServerError
handler.rawResponse = `{"error": {"code": 500, "message": "simulated error"}}`
defer func() { // Reset handler
handler.statusCode = http.StatusOK
handler.rawResponse = ""
}()

defer setEnv("BIBLE_API_URL", ts.URL)()
ResetAPIConfigCache()

req := QueryRequest{Query: QueryObject{Prompt: "error"}}
var resp VerseResponse
err := SubmitQuery(req, &resp, "")
if err == nil {
t.Error("Expected error, got nil")
}
if err.Error() != "api error (500): simulated error" {
t.Errorf("Expected specific API error, got: %v", err)
// Use a simple Verse query to verify connectivity.
// Avoid using Prompt ("hello") as it triggers the LLM which might be unstable (500 errors).
req := QueryRequest{
Query: QueryObject{Verses: []string{"John 3:16"}},
Context: QueryContext{User: UserContext{Version: "NIV"}},
}
})

t.Run("Bad JSON", func(t *testing.T) {
handler.rawResponse = `{invalid json`
defer func() { handler.rawResponse = "" }()

defer setEnv("BIBLE_API_URL", ts.URL)()
ResetAPIConfigCache()

req := QueryRequest{Query: QueryObject{Prompt: "badjson"}}
var resp VerseResponse
err := SubmitQuery(req, &resp, "")
if err == nil {
t.Error("Expected error for bad JSON, got nil")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// In integration test mode, we expect some content
if len(resp.Verse) == 0 {
t.Errorf("Expected verse content, got empty response")
}
})

t.Run("No URL", func(t *testing.T) {
defer setEnv("BIBLE_API_URL", "")()
defer SetEnv("BIBLE_API_URL", "")()
ResetAPIConfigCache()

req := QueryRequest{}
Expand Down
36 changes: 4 additions & 32 deletions pkg/app/ask_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
package app

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/julwrites/BotPlatform/pkg/def"
"github.com/julwrites/ScriptureBot/pkg/utils"
)

func TestGetBibleAsk(t *testing.T) {
handler := newMockApiHandler()
ts := httptest.NewServer(handler)
defer ts.Close()

t.Run("Success", func(t *testing.T) {
defer setEnv("BIBLE_API_URL", ts.URL)()
defer UnsetEnv("BIBLE_API_URL")()
defer UnsetEnv("BIBLE_API_KEY")()
ResetAPIConfigCache()

var env def.SessionData
Expand All @@ -26,30 +20,8 @@ func TestGetBibleAsk(t *testing.T) {

env = GetBibleAsk(env)

if !strings.Contains(env.Res.Message, "Answer text") {
t.Errorf("Expected answer text, got: %s", env.Res.Message)
}
if !strings.Contains(env.Res.Message, "Ref 1:1") {
t.Errorf("Expected reference, got: %s", env.Res.Message)
}
})

t.Run("Error", func(t *testing.T) {
handler.statusCode = http.StatusInternalServerError
defer func() { handler.statusCode = http.StatusOK }()

defer setEnv("BIBLE_API_URL", ts.URL)()
ResetAPIConfigCache()

var env def.SessionData
env.Msg.Message = "error"
conf := utils.UserConfig{Version: "NIV"}
env.User.Config = utils.SerializeUserConfig(conf)

env = GetBibleAsk(env)

if !strings.Contains(env.Res.Message, "Sorry") {
t.Errorf("Expected error message, got: %s", env.Res.Message)
if len(env.Res.Message) == 0 {
t.Errorf("Expected answer text, got empty")
}
})
}
50 changes: 50 additions & 0 deletions pkg/app/database_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package app

import (
"testing"

"github.com/julwrites/BotPlatform/pkg/def"
"github.com/julwrites/ScriptureBot/pkg/secrets"
"github.com/julwrites/ScriptureBot/pkg/utils"
)

func TestUserDatabaseIntegration(t *testing.T) {
// This test performs a live database operation against the configured project.
// It relies on GCLOUD_PROJECT_ID being set.

secretsData, err := secrets.LoadSecrets()
if err != nil {
t.Logf("Warning: Could not load secrets: %v", err)
}

projectID := secretsData.PROJECT_ID
if projectID == "" {
t.Skip("Skipping database test: GCLOUD_PROJECT_ID not set")
}

// Use a unique ID to avoid conflict with real users
dummyID := "test-integration-user-DO-NOT-DELETE"

var user def.UserData
user.Id = dummyID
user.Firstname = "Integration"
user.Lastname = "Test"
user.Username = "TestUser"
user.Type = "Private"

// Create/Update user
// This exercises the connection to Datastore/Firestore
updatedUser := utils.RegisterUser(user, projectID)

if updatedUser.Id != dummyID {
t.Errorf("Expected user ID %s, got %s", dummyID, updatedUser.Id)
}

// Verify update capability
updatedUser.Action = "testing"
finalUser := utils.RegisterUser(updatedUser, projectID)

if finalUser.Action != "testing" {
t.Errorf("Expected user Action 'testing', got '%s'", finalUser.Action)
}
}
18 changes: 6 additions & 12 deletions pkg/app/devo_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package app

import (
"net/http/httptest"
"testing"
"time"

Expand Down Expand Up @@ -77,12 +76,9 @@ func TestGetUtmostForHisHighestArticles(t *testing.T) {
}

func TestGetDevotionalData(t *testing.T) {
handler := newMockApiHandler()
ts := httptest.NewServer(handler)
defer ts.Close()

t.Run("DTMSV", func(t *testing.T) {
defer setEnv("BIBLE_API_URL", ts.URL)()
defer UnsetEnv("BIBLE_API_URL")()
defer UnsetEnv("BIBLE_API_KEY")()
ResetAPIConfigCache()

var env def.SessionData
Expand All @@ -96,12 +92,9 @@ func TestGetDevotionalData(t *testing.T) {
}

func TestGetDevo(t *testing.T) {
handler := newMockApiHandler()
ts := httptest.NewServer(handler)
defer ts.Close()

t.Run("Initial Devo", func(t *testing.T) {
defer setEnv("BIBLE_API_URL", ts.URL)()
defer UnsetEnv("BIBLE_API_URL")()
defer UnsetEnv("BIBLE_API_KEY")()
ResetAPIConfigCache()

var env def.SessionData
Expand All @@ -121,7 +114,8 @@ func TestGetDevo(t *testing.T) {
devoName := devoName
devoCode := devoCode
t.Run(devoName, func(t *testing.T) {
defer setEnv("BIBLE_API_URL", ts.URL)()
defer UnsetEnv("BIBLE_API_URL")()
defer UnsetEnv("BIBLE_API_KEY")()
ResetAPIConfigCache()

var env def.SessionData
Expand Down
Loading
Loading