roboflow working, and added integration tests
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
junk2jive-server
|
||||
.env
|
||||
.direnv
|
||||
coverage.out
|
||||
@@ -29,7 +29,6 @@ func main() {
|
||||
if err != nil {
|
||||
logger.Error("Error starting server %v", err)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
<-stop
|
||||
|
||||
97
coverage.out
97
coverage.out
@@ -1,97 +0,0 @@
|
||||
mode: set
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:19.25,23.2 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:25.36,27.16 2 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:27.16,29.3 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:30.2,30.11 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:33.91,37.45 3 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:37.45,39.3 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.2,41.54 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.54,43.3 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:45.2,47.16 3 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:50.100,51.46 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:51.46,52.72 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:52.72,56.24 3 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:56.24,59.5 2 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:61.4,61.24 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:25.13,31.62 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:31.62,32.31 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:32.31,33.27 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:34.22,38.8 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:41.5,41.13 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:45.2,45.25 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:48.66,49.39 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:49.39,50.20 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:50.20,52.4 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:53.3,55.26 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:55.26,57.4 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:13.69,14.46 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:14.46,15.72 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:15.72,32.47 5 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:32.47,34.5 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:36.4,36.80 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:36.80,38.5 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:38.10,40.5 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:13.43,16.2 2 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:18.38,19.63 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:19.63,21.3 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:22.2,22.12 1 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:25.76,30.2 4 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:32.100,35.2 2 1
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:15.13,26.12 7 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:26.12,29.17 3 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:29.17,31.4 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:35.2,41.45 5 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:41.45,43.3 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:45.2,45.35 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:38.28,42.2 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:45.65,49.61 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:49.61,53.3 3 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:56.2,56.64 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:56.64,59.3 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:62.2,63.16 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:63.16,66.3 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:68.2,72.16 3 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:72.16,75.3 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:78.2,79.38 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:83.77,90.16 4 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:90.16,92.3 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:95.2,100.16 4 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:100.16,102.3 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:103.2,106.38 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:106.38,109.3 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:112.2,113.69 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:113.69,115.3 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:117.2,117.23 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:121.62,123.50 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:123.50,125.3 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:126.2,126.16 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:22.31,23.48 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:23.48,25.3 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:28.28,37.2 3 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:39.38,41.21 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:41.21,43.3 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:43.8,44.34 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:44.34,47.4 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:48.3,48.51 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:50.2,50.13 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:53.81,62.2 3 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:33.53,35.2 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:37.67,58.19 5 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:58.19,60.6 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:62.5,63.19 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:63.19,65.6 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:67.5,72.19 5 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:72.19,74.6 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:75.5,78.72 3 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:78.72,80.6 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.5,82.35 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.35,84.6 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:86.5,86.52 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:28.26,32.2 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:34.44,64.55 13 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:64.55,65.46 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:65.46,68.74 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:68.74,70.5 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:72.4,73.58 2 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:76.2,76.11 1 0
|
||||
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:80.39,82.2 1 0
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
var AllowedOrigins []string
|
||||
|
||||
type Flags struct {
|
||||
// OllamaKey string
|
||||
// RoboflowKey int
|
||||
Port int
|
||||
}
|
||||
|
||||
|
||||
135
internal/config/config_test.go
Normal file
135
internal/config/config_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router"
|
||||
)
|
||||
|
||||
func TestDeclareFlags(t *testing.T) {
|
||||
// Save original command line arguments
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
// Set up test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expected *Flags
|
||||
}{
|
||||
{
|
||||
name: "No arguments",
|
||||
args: []string{"cmd"},
|
||||
expected: &Flags{Port: 0},
|
||||
},
|
||||
{
|
||||
name: "Port flag provided",
|
||||
args: []string{"cmd", "-p", "8080"},
|
||||
expected: &Flags{Port: 8080},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Reset flags for each test
|
||||
flag.CommandLine = flag.NewFlagSet(tc.args[0], flag.ExitOnError)
|
||||
|
||||
// Set the arguments
|
||||
os.Args = tc.args
|
||||
|
||||
// Run the function
|
||||
got := DeclareFlags()
|
||||
|
||||
// Check results
|
||||
if got.Port != tc.expected.Port {
|
||||
t.Errorf("DeclareFlags() Port = %v, want %v", got.Port, tc.expected.Port)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPort(t *testing.T) {
|
||||
// Save original env and restore after test
|
||||
oldPort := os.Getenv("APP_PORT")
|
||||
defer os.Setenv("APP_PORT", oldPort)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
flags *Flags
|
||||
envPort string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Flag port specified",
|
||||
flags: &Flags{Port: 8080},
|
||||
envPort: "9000",
|
||||
expected: ":8080",
|
||||
},
|
||||
{
|
||||
name: "No flag port but env port specified",
|
||||
flags: &Flags{Port: 0},
|
||||
envPort: "9000",
|
||||
expected: ":9000",
|
||||
},
|
||||
{
|
||||
name: "No ports specified - default to 80",
|
||||
flags: &Flags{Port: 0},
|
||||
envPort: "",
|
||||
expected: ":80",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
os.Setenv("APP_PORT", tc.envPort)
|
||||
|
||||
got := ConfigPort(tc.flags)
|
||||
|
||||
if got != tc.expected {
|
||||
t.Errorf("ConfigPort() = %v, want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupServer(t *testing.T) {
|
||||
// Create mock router
|
||||
r := &router.Router{}
|
||||
flags := &Flags{Port: 8080}
|
||||
port := ":8080"
|
||||
|
||||
server := SetupServer(port, r, flags)
|
||||
|
||||
// Verify server was created with correct attributes
|
||||
if server == nil {
|
||||
t.Fatal("SetupServer() returned nil")
|
||||
}
|
||||
|
||||
if server.Addr != port {
|
||||
t.Errorf("SetupServer() created server with Addr = %v, want %v", server.Addr, port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEnv(t *testing.T) {
|
||||
// Create a temporary .env file for testing
|
||||
tempEnvFile := "test.env"
|
||||
content := []byte("TEST_VAR=test_value\n")
|
||||
|
||||
err := os.WriteFile(tempEnvFile, content, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test env file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempEnvFile)
|
||||
|
||||
// Test loading the file (this will fail if it can't load)
|
||||
// Since LoadEnv calls logger.Fatal on error, we can't easily test the failure case
|
||||
LoadEnv(tempEnvFile)
|
||||
|
||||
// Verify the environment variable was loaded
|
||||
if os.Getenv("TEST_VAR") != "test_value" {
|
||||
t.Errorf("LoadEnv() didn't properly load TEST_VAR, got %v, want %v",
|
||||
os.Getenv("TEST_VAR"), "test_value")
|
||||
}
|
||||
}
|
||||
@@ -1,315 +1,290 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Mock HTTP client for testing
|
||||
type mockHTTPClient struct {
|
||||
DoFunc func(req *http.Request) (*http.Response, error)
|
||||
type MockOllama struct {
|
||||
SendRequestFunc func(userMessage string, fileID string) (string, error)
|
||||
}
|
||||
|
||||
func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
return m.DoFunc(req)
|
||||
func (m *MockOllama) SendRequest(userMessage string, fileID string) (string, error) {
|
||||
return m.SendRequestFunc(userMessage, fileID)
|
||||
}
|
||||
|
||||
// Helper function to patch the HTTP client for testing
|
||||
func patchHTTPClient(client interface{}) func() {
|
||||
originalClient := http.DefaultClient
|
||||
http.DefaultClient = client.(*http.Client)
|
||||
return func() {
|
||||
http.DefaultClient = originalClient
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewOllama tests the NewOllama function
|
||||
func TestNewOllama(t *testing.T) {
|
||||
// Save original env and restore after test
|
||||
originalKey := os.Getenv("OLLAMA_API_KEY")
|
||||
defer os.Setenv("OLLAMA_API_KEY", originalKey)
|
||||
// Save original environment and restore it after test
|
||||
origEndpoint := os.Getenv("OLLAMA_API_ENDPOINT")
|
||||
origKey := os.Getenv("OLLAMA_API_KEY")
|
||||
defer func() {
|
||||
os.Setenv("OLLAMA_API_ENDPOINT", origEndpoint)
|
||||
os.Setenv("OLLAMA_API_KEY", origKey)
|
||||
}()
|
||||
|
||||
testKey := "test-api-key"
|
||||
os.Setenv("OLLAMA_API_KEY", testKey)
|
||||
// Test with empty environment
|
||||
os.Unsetenv("OLLAMA_API_ENDPOINT")
|
||||
os.Setenv("OLLAMA_API_KEY", "test-key")
|
||||
|
||||
ollama := NewOllama()
|
||||
if ollama == nil {
|
||||
t.Fatal("Expected NewOllama to return a non-nil value")
|
||||
}
|
||||
ollama := NewOllama()
|
||||
if ollama.OllamaEndpoint != "http://localhost:3000/api/chat/completions" {
|
||||
t.Errorf("Expected default endpoint, got %s", ollama.OllamaEndpoint)
|
||||
}
|
||||
if ollama.OllamaKey != "test-key" {
|
||||
t.Errorf("Expected API key 'test-key', got %s", ollama.OllamaKey)
|
||||
}
|
||||
|
||||
if ollama.OllamaKey != testKey {
|
||||
t.Errorf("Expected OllamaKey to be %q, got %q", testKey, ollama.OllamaKey)
|
||||
}
|
||||
// Test with custom environment
|
||||
os.Setenv("OLLAMA_API_ENDPOINT", "https://custom-endpoint.com")
|
||||
os.Setenv("OLLAMA_API_KEY", "custom-key")
|
||||
|
||||
ollama = NewOllama()
|
||||
if ollama.OllamaEndpoint != "https://custom-endpoint.com" {
|
||||
t.Errorf("Expected custom endpoint, got %s", ollama.OllamaEndpoint)
|
||||
}
|
||||
if ollama.OllamaKey != "custom-key" {
|
||||
t.Errorf("Expected API key 'custom-key', got %s", ollama.OllamaKey)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendRequest tests the SendRequest method
|
||||
func TestSendRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
userMessage string
|
||||
fileID string
|
||||
setupMock func() *mockHTTPClient
|
||||
expectedResult string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Successful request without file",
|
||||
userMessage: "Hello, world!",
|
||||
fileID: "",
|
||||
setupMock: func() *mockHTTPClient {
|
||||
return &mockHTTPClient{
|
||||
DoFunc: func(req *http.Request) (*http.Response, error) {
|
||||
// Verify request has correct content and headers
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Expected Content-Type header to be application/json")
|
||||
}
|
||||
if req.Header.Get("Authorization") != "Bearer test-key" {
|
||||
t.Errorf("Expected Authorization header to be Bearer test-key")
|
||||
}
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify headers
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Expected Content-Type header to be application/json")
|
||||
}
|
||||
if r.Header.Get("Authorization") != "Bearer test-key" {
|
||||
t.Errorf("Expected Authorization header to be 'Bearer test-key'")
|
||||
}
|
||||
|
||||
// Check request body
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
var reqBody ChatCompletionRequest
|
||||
if err := json.Unmarshal(body, &reqBody); err != nil {
|
||||
t.Errorf("Failed to unmarshal request body: %v", err)
|
||||
}
|
||||
if reqBody.Model != "gemma3:12b" {
|
||||
t.Errorf("Expected model to be gemma3:12b, got %s", reqBody.Model)
|
||||
}
|
||||
if len(reqBody.Messages) != 1 || reqBody.Messages[0].Content != "Hello, world!" {
|
||||
t.Errorf("Expected message content to be 'Hello, world!'")
|
||||
}
|
||||
if len(reqBody.Files) != 0 {
|
||||
t.Errorf("Expected no files, got %d", len(reqBody.Files))
|
||||
}
|
||||
// Verify request body
|
||||
var reqBody ChatCompletionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||
t.Errorf("Error decoding request body: %v", err)
|
||||
}
|
||||
|
||||
// Return successful response
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"response": "Hello from Ollama!"}`)),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedResult: `{"response": "Hello from Ollama!"}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Successful request with file",
|
||||
userMessage: "Analyze this file",
|
||||
fileID: "file123",
|
||||
setupMock: func() *mockHTTPClient {
|
||||
return &mockHTTPClient{
|
||||
DoFunc: func(req *http.Request) (*http.Response, error) {
|
||||
// Check request body to ensure file was included
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
var reqBody ChatCompletionRequest
|
||||
json.Unmarshal(body, &reqBody)
|
||||
|
||||
if len(reqBody.Files) != 1 || reqBody.Files[0].ID != "file123" {
|
||||
t.Errorf("Expected file ID to be file123")
|
||||
}
|
||||
if reqBody.Model != "gemma3:12b" {
|
||||
t.Errorf("Expected model 'gemma3:12b', got %s", reqBody.Model)
|
||||
}
|
||||
if len(reqBody.Messages) != 1 {
|
||||
t.Errorf("Expected 1 message, got %d", len(reqBody.Messages))
|
||||
}
|
||||
if reqBody.Messages[0].Role != "user" {
|
||||
t.Errorf("Expected role 'user', got %s", reqBody.Messages[0].Role)
|
||||
}
|
||||
|
||||
// Return successful response
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"response": "File analysis complete"}`)),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedResult: `{"response": "File analysis complete"}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP error response",
|
||||
userMessage: "Hello, world!",
|
||||
fileID: "",
|
||||
setupMock: func() *mockHTTPClient {
|
||||
return &mockHTTPClient{
|
||||
DoFunc: func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"error": "Bad request"}`)),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
},
|
||||
expectedResult: "",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
// Check if fileID was properly included
|
||||
if r.URL.Query().Get("fileId") == "test-file" {
|
||||
if len(reqBody.Files) != 1 {
|
||||
t.Errorf("Expected 1 file, got %d", len(reqBody.Files))
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create mock client
|
||||
mockClient := tc.setupMock()
|
||||
|
||||
// Create a custom HTTP client and replace the default
|
||||
client := &http.Client{
|
||||
Transport: &mockTransport{mockClient: mockClient},
|
||||
}
|
||||
|
||||
// Initialize ollama with test key
|
||||
ollama := &Ollama{OllamaKey: "test-key"}
|
||||
|
||||
// Mock response.RespondWithError to prevent nil pointer dereference
|
||||
originalRespondWithError := response.RespondWithError
|
||||
response.RespondWithError = func(w http.ResponseWriter, r *http.Request, status int, message string, err error) {}
|
||||
defer func() { response.RespondWithError = originalRespondWithError }()
|
||||
|
||||
// Call the function
|
||||
result, err := ollama.SendRequest(tc.userMessage, tc.fileID)
|
||||
|
||||
// Check results
|
||||
if tc.expectError && err == nil {
|
||||
t.Error("Expected an error but got none")
|
||||
}
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Errorf("Did not expect an error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectError && result != tc.expectedResult {
|
||||
t.Errorf("Expected result %q, got %q", tc.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
// Return mock response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"response": "This is a test response"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create Ollama client with mock server URL
|
||||
ollama := &Ollama{
|
||||
OllamaKey: "test-key",
|
||||
OllamaEndpoint: server.URL,
|
||||
}
|
||||
|
||||
// Test without fileID
|
||||
resp, err := ollama.SendRequest("Hello", "")
|
||||
if err != nil {
|
||||
t.Errorf("SendRequest failed: %v", err)
|
||||
}
|
||||
if resp != `{"response": "This is a test response"}` {
|
||||
t.Errorf("Unexpected response: %s", resp)
|
||||
}
|
||||
|
||||
// Test with fileID
|
||||
resp, err = ollama.SendRequest("Hello with file", "test-file")
|
||||
if err != nil {
|
||||
t.Errorf("SendRequest with fileID failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom transport that uses our mockClient
|
||||
type mockTransport struct {
|
||||
mockClient *mockHTTPClient
|
||||
func TestSendRequestError(t *testing.T) {
|
||||
// Create mock server that returns an error
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal Server Error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create Ollama client with mock server URL
|
||||
ollama := &Ollama{
|
||||
OllamaKey: "test-key",
|
||||
OllamaEndpoint: server.URL,
|
||||
}
|
||||
|
||||
// Test error handling
|
||||
_, err := ollama.SendRequest("Hello", "")
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return m.mockClient.Do(req)
|
||||
// TestOllamaRequestHandlerIntegration tests the complete flow of the OllamaRequest handler
|
||||
func TestOllamaRequestHandlerIntegration(t *testing.T) {
|
||||
// Save original NewOllama function and restore it after the test
|
||||
originalNewOllama := NewOllama
|
||||
defer func() {
|
||||
NewOllama = originalNewOllama
|
||||
}()
|
||||
|
||||
// Set up test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
requestBody string
|
||||
mockResponse string
|
||||
mockError error
|
||||
expectedStatus int
|
||||
expectedBody string
|
||||
}{
|
||||
{
|
||||
name: "Successful request",
|
||||
requestBody: `{"message": "Hello, AI!", "fileId": "test-file"}`,
|
||||
mockResponse: `{"choices":[{"message":{"content":"Hello, human!"}}]}`,
|
||||
mockError: nil,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: `{"response":"{\"choices\":[{\"message\":{\"content\":\"Hello, human!\"}}]}"}`,
|
||||
},
|
||||
{
|
||||
name: "Invalid request body",
|
||||
requestBody: `{"message": }`, // Invalid JSON
|
||||
mockResponse: "",
|
||||
mockError: nil,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: `{"error":"Invalid request body"}`,
|
||||
},
|
||||
{
|
||||
name: "API Error",
|
||||
requestBody: `{"message": "Hello"}`,
|
||||
mockResponse: "",
|
||||
mockError: fmt.Errorf("API error"),
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: `{"error":"Error sending request to Ollama API"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Override the NewOllama function to return our mock
|
||||
NewOllama = func() *Ollama {
|
||||
return &Ollama{
|
||||
OllamaKey: "mock-key",
|
||||
OllamaEndpoint: "mock-endpoint",
|
||||
}
|
||||
}
|
||||
|
||||
// Create a mock SendRequest function
|
||||
originalSendRequest := (*Ollama).SendRequest
|
||||
defer func() {
|
||||
(*Ollama).SendRequest = originalSendRequest
|
||||
}()
|
||||
|
||||
(*Ollama).SendRequest = func(o *Ollama, userMessage string, fileID string) (string, error) {
|
||||
return tc.mockResponse, tc.mockError
|
||||
}
|
||||
|
||||
// Create a request with the test case body
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/ollama", strings.NewReader(tc.requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create a recorder to capture the response
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Call the handler
|
||||
OllamaRequest(w, req)
|
||||
|
||||
// Get the result
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check the status code
|
||||
if resp.StatusCode != tc.expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d", tc.expectedStatus, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Check the response body
|
||||
var respBody map[string]string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||
t.Fatalf("Failed to decode response body: %v", err)
|
||||
}
|
||||
|
||||
// For successful case, verify the response contains the expected content
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
if _, ok := respBody["response"]; !ok {
|
||||
t.Errorf("Response doesn't contain 'response' field")
|
||||
}
|
||||
} else {
|
||||
// For error cases, check if error message is included
|
||||
if _, ok := respBody["error"]; !ok {
|
||||
t.Errorf("Error response doesn't contain 'error' field")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOllamaRequest tests the OllamaRequest HTTP handler
|
||||
func TestOllamaRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody string
|
||||
setupMock func() (*Ollama, func())
|
||||
expectedStatusCode int
|
||||
expectedResponse string
|
||||
}{
|
||||
{
|
||||
name: "Valid request",
|
||||
requestBody: `{"message": "Hello", "fileId": ""}`,
|
||||
setupMock: func() (*Ollama, func()) {
|
||||
// Create a mock Ollama that returns a successful response
|
||||
mockOllama := &Ollama{}
|
||||
|
||||
// Save the original NewOllama function and replace it
|
||||
originalNewOllama := NewOllama
|
||||
NewOllama = func() *Ollama {
|
||||
return mockOllama
|
||||
}
|
||||
|
||||
// Save the original SendRequest method and replace it
|
||||
originalSendRequest := mockOllama.SendRequest
|
||||
mockOllama.SendRequest = func(userMessage, fileID string) (string, error) {
|
||||
return `{"content": "Hello from Ollama!"}`, nil
|
||||
}
|
||||
|
||||
// Return the mock and a cleanup function
|
||||
return mockOllama, func() {
|
||||
NewOllama = originalNewOllama
|
||||
mockOllama.SendRequest = originalSendRequest
|
||||
}
|
||||
},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedResponse: `{"response":{"content": "Hello from Ollama!"}}`,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON request",
|
||||
requestBody: `{"message": "Hello"`, // Missing closing brace
|
||||
setupMock: func() (*Ollama, func()) {
|
||||
mockOllama := &Ollama{}
|
||||
originalNewOllama := NewOllama
|
||||
NewOllama = func() *Ollama {
|
||||
return mockOllama
|
||||
}
|
||||
return mockOllama, func() {
|
||||
NewOllama = originalNewOllama
|
||||
}
|
||||
},
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
expectedResponse: "", // We don't care about the exact error message
|
||||
},
|
||||
{
|
||||
name: "API error",
|
||||
requestBody: `{"message": "Error", "fileId": ""}`,
|
||||
setupMock: func() (*Ollama, func()) {
|
||||
mockOllama := &Ollama{}
|
||||
originalNewOllama := NewOllama
|
||||
NewOllama = func() *Ollama {
|
||||
return mockOllama
|
||||
}
|
||||
originalSendRequest := mockOllama.SendRequest
|
||||
mockOllama.SendRequest = func(userMessage, fileID string) (string, error) {
|
||||
return "", &httpError{message: "API error", statusCode: http.StatusInternalServerError}
|
||||
}
|
||||
return mockOllama, func() {
|
||||
NewOllama = originalNewOllama
|
||||
mockOllama.SendRequest = originalSendRequest
|
||||
}
|
||||
},
|
||||
expectedStatusCode: http.StatusInternalServerError,
|
||||
expectedResponse: "", // We don't care about the exact error message
|
||||
},
|
||||
}
|
||||
// TestEndToEndIntegration tests the actual API connection if environment variables are set
|
||||
func TestEndToEndIntegration(t *testing.T) {
|
||||
// Skip this test unless explicitly enabled with an environment variable
|
||||
if os.Getenv("ENABLE_E2E_TESTS") != "true" {
|
||||
t.Skip("Skipping end-to-end test; set ENABLE_E2E_TESTS=true to run")
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Setup mock
|
||||
_, cleanup := tc.setupMock()
|
||||
defer cleanup()
|
||||
// Create a real request
|
||||
reqBody := map[string]string{
|
||||
"message": "Hello, this is a test message",
|
||||
}
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
// Create request and response recorder
|
||||
req := httptest.NewRequest("POST", "/api/ollama", strings.NewReader(tc.requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
// Create a request to our handler
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/ollama", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Call the handler
|
||||
OllamaRequest(w, req)
|
||||
// Create a recorder
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Check status code
|
||||
if w.Code != tc.expectedStatusCode {
|
||||
t.Errorf("Expected status code %d, got %d", tc.expectedStatusCode, w.Code)
|
||||
}
|
||||
// Call the actual handler (which will make a real API call)
|
||||
OllamaRequest(w, req)
|
||||
|
||||
// For successful responses, check the response body
|
||||
if tc.expectedStatusCode == http.StatusOK {
|
||||
// Remove whitespace for comparison
|
||||
expectedNoSpace := strings.ReplaceAll(tc.expectedResponse, " ", "")
|
||||
actualNoSpace := strings.ReplaceAll(w.Body.String(), " ", "")
|
||||
actualNoSpace = strings.ReplaceAll(actualNoSpace, "\n", "")
|
||||
|
||||
if expectedNoSpace != actualNoSpace {
|
||||
t.Errorf("Expected response %q, got %q", tc.expectedResponse, w.Body.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// Check response
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Basic assertions
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", resp.Status)
|
||||
|
||||
// Print error body for debugging
|
||||
var errorResp map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err == nil {
|
||||
t.Logf("Error response: %v", errorResp)
|
||||
}
|
||||
} else {
|
||||
var respBody map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||
t.Errorf("Failed to decode response body: %v", err)
|
||||
} else {
|
||||
t.Logf("Successful API response received: %v", respBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper error type for testing
|
||||
type httpError struct {
|
||||
message string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (e *httpError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger"
|
||||
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// ImageRequest represents the incoming JSON request with image data
|
||||
@@ -22,10 +24,6 @@ type RoboflowResponse struct {
|
||||
Predictions []struct {
|
||||
Class string `json:"class"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
} `json:"predictions"`
|
||||
}
|
||||
|
||||
@@ -34,34 +32,46 @@ type Service struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
|
||||
func init() {
|
||||
// Load .env file if it exists
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
logger.Debug("No .env file found or error loading it: ", err)
|
||||
} else {
|
||||
logger.Debug(".env file loaded successfully")
|
||||
}
|
||||
}
|
||||
// NewService creates a new Roboflow service instance
|
||||
func NewService() *Service {
|
||||
apiKey := os.Getenv("ROBOFLOW_API_KEY")
|
||||
if apiKey == "" {
|
||||
logger.Error("ROBOFLOW_API_KEY environment variable not set")
|
||||
}
|
||||
return &Service{
|
||||
apiKey: os.Getenv("ROBOFLOW_API_KEY"),
|
||||
apiKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleImageRequest processes the JSON image request
|
||||
func HandleImageRequest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Decode the JSON request
|
||||
var req ImageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid JSON request: %v", err)
|
||||
http.Error(w, "Invalid JSON request: "+err.Error(), http.StatusBadRequest)
|
||||
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid JSON request", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if image was provided
|
||||
if err := json.Unmarshal([]byte(req.Image), &req); err != nil {
|
||||
response.RespondWithError(w, r, http.StatusBadRequest, "No image provided in request", err)
|
||||
if req.Image == "" {
|
||||
response.RespondWithError(w, r, http.StatusBadRequest, "No image provided in request", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode base64 image
|
||||
imageData, err := base64.StdEncoding.DecodeString(req.Image)
|
||||
if err != nil {
|
||||
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid base64 image: %v ", err)
|
||||
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid base64 image", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,7 +80,7 @@ func HandleImageRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// Process the image with Roboflow
|
||||
responser, err := service.AnalyzeImage(imageData)
|
||||
if err != nil {
|
||||
response.RespondWithError(w, r, http.StatusInternalServerError, "Error processing image: %v", err)
|
||||
response.RespondWithError(w, r, http.StatusInternalServerError, "Error processing image", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
260
internal/roboflow/roboflow_test.go
Normal file
260
internal/roboflow/roboflow_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package roboflow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
// Save original env and restore after test
|
||||
originalAPIKey := os.Getenv("ROBOFLOW_API_KEY")
|
||||
defer os.Setenv("ROBOFLOW_API_KEY", originalAPIKey)
|
||||
|
||||
// Set test API key
|
||||
testAPIKey := "test-api-key"
|
||||
os.Setenv("ROBOFLOW_API_KEY", testAPIKey)
|
||||
|
||||
// Create new service
|
||||
service := NewService()
|
||||
|
||||
// Check if API key is set correctly
|
||||
if service.apiKey != testAPIKey {
|
||||
t.Errorf("Expected apiKey to be %s, got %s", testAPIKey, service.apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDetectedObjects(t *testing.T) {
|
||||
// Create test response
|
||||
response := &RoboflowResponse{
|
||||
Predictions: []struct {
|
||||
Class string `json:"class"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}{
|
||||
{Class: "bottle", Confidence: 0.95},
|
||||
{Class: "cup", Confidence: 0.85},
|
||||
},
|
||||
}
|
||||
|
||||
// Get detected objects
|
||||
objects := GetDetectedObjects(response)
|
||||
|
||||
// Check if objects are extracted correctly
|
||||
expected := []string{"bottle", "cup"}
|
||||
if !reflect.DeepEqual(objects, expected) {
|
||||
t.Errorf("Expected objects %v, got %v", expected, objects)
|
||||
}
|
||||
}
|
||||
|
||||
// MockHTTPClient allows us to mock HTTP responses
|
||||
type MockHTTPClient struct {
|
||||
DoFunc func(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
return m.DoFunc(req)
|
||||
}
|
||||
|
||||
func TestAnalyzeImage(t *testing.T) {
|
||||
// Setup
|
||||
originalClient := http.DefaultClient
|
||||
defer func() { http.DefaultClient = originalClient }()
|
||||
|
||||
// Test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
responseStatus int
|
||||
responseBody string
|
||||
expectedError bool
|
||||
expectedResult *RoboflowResponse
|
||||
}{
|
||||
{
|
||||
name: "Successful API response",
|
||||
responseStatus: http.StatusOK,
|
||||
responseBody: `{"predictions":[{"class":"bottle","confidence":0.95}]}`,
|
||||
expectedError: false,
|
||||
expectedResult: &RoboflowResponse{
|
||||
Predictions: []struct {
|
||||
Class string `json:"class"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}{
|
||||
{Class: "bottle", Confidence: 0.95},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "API error response",
|
||||
responseStatus: http.StatusInternalServerError,
|
||||
responseBody: `{"error":"Internal server error"}`,
|
||||
expectedError: true,
|
||||
expectedResult: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Mock HTTP client
|
||||
mockClient := &http.Client{
|
||||
Transport: &mockTransport{
|
||||
response: &http.Response{
|
||||
StatusCode: tc.responseStatus,
|
||||
Body: io.NopCloser(strings.NewReader(tc.responseBody)),
|
||||
Header: make(http.Header),
|
||||
},
|
||||
},
|
||||
}
|
||||
http.DefaultClient = mockClient
|
||||
|
||||
service := &Service{apiKey: "api-key"}
|
||||
result, err := service.AnalyzeImage([]byte("test-image-data"))
|
||||
|
||||
// Check error
|
||||
if tc.expectedError && err == nil {
|
||||
t.Error("Expected error but got nil")
|
||||
}
|
||||
if !tc.expectedError && err != nil {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
// Check result
|
||||
if !tc.expectedError {
|
||||
if result == nil {
|
||||
t.Error("Expected result but got nil")
|
||||
} else if len(result.Predictions) != len(tc.expectedResult.Predictions) {
|
||||
t.Errorf("Expected %d predictions, got %d",
|
||||
len(tc.expectedResult.Predictions),
|
||||
len(result.Predictions))
|
||||
} else {
|
||||
for i, pred := range result.Predictions {
|
||||
expected := tc.expectedResult.Predictions[i]
|
||||
if pred.Class != expected.Class || pred.Confidence != expected.Confidence {
|
||||
t.Errorf("Expected prediction %v, got %v", expected, pred)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mockTransport is a mock implementation of RoundTripper
|
||||
type mockTransport struct {
|
||||
response *http.Response
|
||||
}
|
||||
|
||||
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return m.response, nil
|
||||
}
|
||||
|
||||
func TestHandleImageRequest(t *testing.T) {
|
||||
// Test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
requestBody string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Valid request",
|
||||
requestBody: `{"image":"dGVzdC1pbWFnZS1kYXRh"}`, // Base64 of "test-image-data"
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
requestBody: `{invalid-json}`,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Empty image",
|
||||
requestBody: `{"image":""}`,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Invalid base64",
|
||||
requestBody: `{"image":"not-base64"}`,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
// Set up a mock for AnalyzeImage
|
||||
var AnalyzeImage func(imageData []byte) (*RoboflowResponse, error)
|
||||
originalAnalyzeImage := AnalyzeImage
|
||||
defer func() {
|
||||
AnalyzeImage = originalAnalyzeImage
|
||||
}()
|
||||
|
||||
AnalyzeImage = func(imageData []byte) (*RoboflowResponse, error) {
|
||||
return &RoboflowResponse{
|
||||
Predictions: []struct {
|
||||
Class string `json:"class"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}{
|
||||
{Class: "test-class", Confidence: 0.95},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create request
|
||||
req, err := http.NewRequest("POST", "/analyze", bytes.NewBufferString(tc.requestBody))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Call handler
|
||||
handler := http.HandlerFunc(HandleImageRequest)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status
|
||||
if status := rr.Code; status != tc.expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d", tc.expectedStatus, status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoboflowIntegration(t *testing.T) {
|
||||
|
||||
if os.Getenv("ROBOFLOW_API_KEY") == "" {
|
||||
t.Skip("Skipping test; ROBOFLOW_API_KEY not set")
|
||||
}
|
||||
|
||||
t.Run("AnalyzeImage with valid image", func(t *testing.T) {
|
||||
// Load a test image (you should place a suitable test image in testdata/)
|
||||
imageData, err := os.ReadFile("/home/rogueking/Documents/junk2jive-server/test.png")
|
||||
require.NoError(t, err, "Failed to load test image")
|
||||
|
||||
service := NewService()
|
||||
resp, err := service.AnalyzeImage(imageData)
|
||||
|
||||
require.NoError(t, err, "AnalyzeImage failed")
|
||||
require.NotNil(t, resp, "Response should not be nil")
|
||||
assert.NotEmpty(t, resp.Predictions, "Expected some predictions")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMain sets up and tears down test environment
|
||||
func TestMain(m *testing.M) {
|
||||
// Setup
|
||||
originalAPIKey := os.Getenv("ROBOFLOW_API_KEY")
|
||||
os.Setenv("ROBOFLOW_API_KEY", "")
|
||||
|
||||
// Run tests
|
||||
code := m.Run()
|
||||
|
||||
// Teardown
|
||||
os.Setenv("ROBOFLOW_API_KEY", originalAPIKey)
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
153
internal/router/router_test.go
Normal file
153
internal/router/router_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRouter(t *testing.T) {
|
||||
r := NewRouter()
|
||||
|
||||
assert.NotNil(t, r, "Router should not be nil")
|
||||
assert.NotNil(t, r.router, "Chi router should not be nil")
|
||||
assert.Nil(t, r.logger, "Logger should be nil initially")
|
||||
assert.Nil(t, r.rateLimiter, "RateLimiter should be nil initially")
|
||||
}
|
||||
|
||||
func TestSetupRouter(t *testing.T) {
|
||||
origins := []string{"http://localhost:3000"}
|
||||
r := SetupRouter(origins)
|
||||
|
||||
assert.NotNil(t, r, "Router should not be nil")
|
||||
assert.NotNil(t, r.router, "Chi router should not be nil")
|
||||
assert.NotNil(t, r.logger, "Logger should not be nil")
|
||||
assert.NotNil(t, r.rateLimiter, "RateLimiter should not be nil")
|
||||
}
|
||||
|
||||
func TestGetRouter(t *testing.T) {
|
||||
r := NewRouter()
|
||||
chiRouter := r.GetRouter()
|
||||
|
||||
assert.Equal(t, r.router, chiRouter, "GetRouter should return the router field")
|
||||
}
|
||||
|
||||
func TestCoffeeEndpoint(t *testing.T) {
|
||||
origins := []string{"http://localhost:3000"}
|
||||
r := SetupRouter(origins)
|
||||
|
||||
// Create a test server
|
||||
ts := httptest.NewServer(r.GetRouter())
|
||||
defer ts.Close()
|
||||
|
||||
// Make request to the coffee endpoint
|
||||
resp, err := http.Get(ts.URL + "/v1/api/coffee")
|
||||
require.NoError(t, err, "Error making request to coffee endpoint")
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
assert.Equal(t, http.StatusTeapot, resp.StatusCode, "Should return teapot status")
|
||||
|
||||
// Check response body
|
||||
var responseBody map[string]string
|
||||
err = json.NewDecoder(resp.Body).Decode(&responseBody)
|
||||
require.NoError(t, err, "Error decoding response body")
|
||||
|
||||
assert.Equal(t, "I'm A Teapot!", responseBody["error"], "Response should contain teapot message")
|
||||
}
|
||||
|
||||
func TestCORSMiddleware(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
origins []string
|
||||
requestOrigin string
|
||||
expectedHeader string
|
||||
}{
|
||||
{
|
||||
name: "Allowed origin",
|
||||
origins: []string{"http://allowed-origin.com"},
|
||||
requestOrigin: "http://allowed-origin.com",
|
||||
expectedHeader: "http://allowed-origin.com",
|
||||
},
|
||||
{
|
||||
name: "Multiple allowed origins",
|
||||
origins: []string{"http://origin1.com", "http://origin2.com"},
|
||||
requestOrigin: "http://origin2.com",
|
||||
expectedHeader: "http://origin2.com",
|
||||
},
|
||||
{
|
||||
name: "Wildcard origin",
|
||||
origins: []string{"*"},
|
||||
requestOrigin: "http://any-origin.com",
|
||||
expectedHeader: "*",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := SetupRouter(tc.origins)
|
||||
|
||||
req := httptest.NewRequest("OPTIONS", "/v1/api/coffee", nil)
|
||||
req.Header.Set("Origin", tc.requestOrigin)
|
||||
req.Header.Set("Access-Control-Request-Method", "GET")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
r.GetRouter().ServeHTTP(rr, req)
|
||||
|
||||
// For wildcard origin or matching origin, CORS headers should be set
|
||||
if tc.origins[0] == "*" || contains(tc.origins, tc.requestOrigin) {
|
||||
assert.Equal(t, tc.expectedHeader, rr.Header().Get("Access-Control-Allow-Origin"),
|
||||
"CORS origin header should match expected value")
|
||||
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Methods"), "GET",
|
||||
"CORS methods should contain GET")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouterMiddlewares(t *testing.T) {
|
||||
r := SetupRouter([]string{"http://localhost:3000"})
|
||||
|
||||
// Check if the router has middleware registered (indirect test)
|
||||
// We can't easily test the actual middleware without mocking, but we can verify
|
||||
// the router itself has routes registered
|
||||
|
||||
chiRouter := r.GetRouter()
|
||||
routes := getAllRoutes(chiRouter)
|
||||
|
||||
assert.Contains(t, routes, "/v1/api/coffee", "Coffee endpoint should be registered")
|
||||
assert.Contains(t, routes, "/v1/api/text", "Text endpoint should be registered")
|
||||
assert.Contains(t, routes, "/v1/api/visual", "Visual endpoint should be registered")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// contains checks if a slice contains a specific string
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getAllRoutes is a helper to extract all routes from a chi router
|
||||
func getAllRoutes(router *chi.Mux) []string {
|
||||
var routes []string
|
||||
|
||||
// This is a simplified way to check for routes
|
||||
// Chi doesn't expose routes directly, so this is a proxy check
|
||||
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
routes = append(routes, route)
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = chi.Walk(router, walkFunc)
|
||||
return routes
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type OpenAIService struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
type OpenAIRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type OpenAIResponse struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
func NewOpenAIService(apiKey string) *OpenAIService {
|
||||
return &OpenAIService{apiKey: apiKey}
|
||||
}
|
||||
|
||||
func (s *OpenAIService) GenerateDIY(query string) (string, error) {
|
||||
url := "https://api.openai.com/v1/chat/completions"
|
||||
|
||||
prompt := fmt.Sprintf("Generate creative DIY project ideas to repurpose or recycle the following: %s. Provide detailed instructions, materials needed, and steps to create each project.", query)
|
||||
|
||||
requestBody := OpenAIRequest{
|
||||
Model: "gpt-4", // or "gpt-3.5-turbo" depending on your needs/access
|
||||
Messages: []Message{
|
||||
{
|
||||
Role: "system",
|
||||
Content: "You are a creative DIY expert who helps people repurpose and recycle items into useful projects.",
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: 0.7,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.apiKey))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response OpenAIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(response.Choices) == 0 {
|
||||
return "", fmt.Errorf("no response from OpenAI")
|
||||
}
|
||||
|
||||
return response.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user