Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupSocialAuthorizationRouter() *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.RedirectTrailingSlash = false
|
||||
instagramHandler := NewInstagramChannelHandler(nil, nil, nil, nil)
|
||||
tiktokHandler := NewTikTokChannelHandler(nil, nil, nil, nil)
|
||||
r.POST("/api/v1/accounts/:account_id/instagram/authorization", instagramHandler.ChatwootAuthorization)
|
||||
r.POST("/api/v1/accounts/:account_id/tiktok/authorization", tiktokHandler.ChatwootAuthorization)
|
||||
return r
|
||||
}
|
||||
|
||||
func TestInstagramAuthorization_ReturnsChatwootPayload(t *testing.T) {
|
||||
t.Setenv("INSTAGRAM_APP_ID", "instagram-client")
|
||||
t.Setenv("INSTAGRAM_APP_SECRET", "instagram-secret")
|
||||
t.Setenv("FRONTEND_URL", "https://app.example.test/")
|
||||
|
||||
r := setupSocialAuthorizationRouter()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/42/instagram/authorization", strings.NewReader(`{"return_to":"onboarding"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, true, resp["success"])
|
||||
|
||||
parsed, err := url.Parse(resp["url"].(string))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://api.instagram.com/oauth/authorize", parsed.Scheme+"://"+parsed.Host+parsed.Path)
|
||||
query := parsed.Query()
|
||||
assert.Equal(t, "instagram-client", query.Get("client_id"))
|
||||
assert.Equal(t, "https://app.example.test/instagram/callback", query.Get("redirect_uri"))
|
||||
assert.Equal(t, "instagram_business_basic,instagram_business_manage_messages", query.Get("scope"))
|
||||
assert.Equal(t, "0", query.Get("enable_fb_login"))
|
||||
assert.Equal(t, "1", query.Get("force_authentication"))
|
||||
assert.Equal(t, "code", query.Get("response_type"))
|
||||
assertSocialSignedState(t, query.Get("state"), "instagram-secret", 42, "onboarding")
|
||||
}
|
||||
|
||||
func TestTikTokAuthorization_ReturnsChatwootPayload(t *testing.T) {
|
||||
t.Setenv("TIKTOK_APP_ID", "tiktok-client")
|
||||
t.Setenv("TIKTOK_APP_SECRET", "tiktok-secret")
|
||||
t.Setenv("FRONTEND_URL", "https://app.example.test")
|
||||
|
||||
r := setupSocialAuthorizationRouter()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/42/tiktok/authorization?return_to=onboarding", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, true, resp["success"])
|
||||
|
||||
parsed, err := url.Parse(resp["url"].(string))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://www.tiktok.com/v2/auth/authorize", parsed.Scheme+"://"+parsed.Host+parsed.Path)
|
||||
query := parsed.Query()
|
||||
assert.Equal(t, "tiktok-client", query.Get("client_id"))
|
||||
assert.Equal(t, "tiktok-client", query.Get("client_key"))
|
||||
assert.Equal(t, "https://app.example.test/tiktok/callback", query.Get("redirect_uri"))
|
||||
assert.Equal(t, tiktokAuthorizationScope, query.Get("scope"))
|
||||
assert.Equal(t, "code", query.Get("response_type"))
|
||||
assertSocialSignedState(t, query.Get("state"), "tiktok-secret", 42, "onboarding")
|
||||
}
|
||||
|
||||
func TestSocialAuthorization_BadAccountID(t *testing.T) {
|
||||
r := setupSocialAuthorizationRouter()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/abc/instagram/authorization", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
errBody := resp["error"].(map[string]interface{})
|
||||
assert.Contains(t, errBody["message"], "invalid account_id")
|
||||
}
|
||||
|
||||
func TestSocialAuthorization_NotConfigured(t *testing.T) {
|
||||
t.Setenv("INSTAGRAM_APP_ID", "")
|
||||
t.Setenv("INSTAGRAM_APP_SECRET", "")
|
||||
r := setupSocialAuthorizationRouter()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/42/instagram/authorization", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, false, resp["success"])
|
||||
}
|
||||
|
||||
func assertSocialSignedState(t *testing.T, state string, secret string, accountID float64, returnTo string) {
|
||||
t.Helper()
|
||||
claims := jwt.MapClaims{}
|
||||
token, err := jwt.ParseWithClaims(state, claims, func(token *jwt.Token) (any, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, token.Valid)
|
||||
assert.Equal(t, accountID, claims["sub"])
|
||||
assert.Equal(t, returnTo, claims["return_to"])
|
||||
}
|
||||
Reference in New Issue
Block a user