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,188 @@
|
||||
package facebook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestIsIGCommentEvent verifies that comment event types are correctly identified.
|
||||
func TestIsIGCommentEvent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
eventType WebhookEventType
|
||||
expected bool
|
||||
}{
|
||||
{"comment event", EventIGComment, true},
|
||||
{"comment reply event", EventIGCommentReply, true},
|
||||
{"comment deleted event", EventIGCommentDeleted, true},
|
||||
{"FB message event", EventFBMessage, false},
|
||||
{"IG message event", EventIGMessage, false},
|
||||
{"IG postback event", EventIGPostback, false},
|
||||
{"delivery event", EventFBDelivery, false},
|
||||
{"read event", EventFBRead, false},
|
||||
{"pass thread event", EventFBPassThread, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := &ParsedWebhookEvent{EventType: tt.eventType}
|
||||
assert.Equal(t, tt.expected, IsIGCommentEvent(event))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseIGCommentChangeValue verifies parsing of Instagram comment change values
|
||||
// from raw map[string]interface{} (as received from Facebook Graph API webhook).
|
||||
func TestParseIGCommentChangeValue(t *testing.T) {
|
||||
t.Run("new comment", func(t *testing.T) {
|
||||
value := map[string]interface{}{
|
||||
"from": map[string]interface{}{
|
||||
"id": "17841400123456789",
|
||||
"username": "testuser",
|
||||
},
|
||||
"media_id": "18027993912345678",
|
||||
"id": "17870412345678901",
|
||||
"text": "Great photo!",
|
||||
"timestamp": "2023-09-18T12:00:00+0000",
|
||||
}
|
||||
|
||||
result, err := parseIGCommentChangeValue(value)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "17841400123456789", result.From.ID)
|
||||
assert.Equal(t, "testuser", result.From.Username)
|
||||
assert.Equal(t, "18027993912345678", result.MediaID)
|
||||
assert.Equal(t, "17870412345678901", result.CommentID)
|
||||
assert.Equal(t, "Great photo!", result.Text)
|
||||
assert.Equal(t, "2023-09-18T12:00:00+0000", result.Timestamp)
|
||||
assert.False(t, result.IsDeleted)
|
||||
})
|
||||
|
||||
t.Run("comment reply with parent_id", func(t *testing.T) {
|
||||
value := map[string]interface{}{
|
||||
"from": map[string]interface{}{
|
||||
"id": "17841400987654321",
|
||||
"username": "replyuser",
|
||||
},
|
||||
"media_id": "18027993912345678",
|
||||
"id": "17870498765432109",
|
||||
"text": "Thanks for the comment!",
|
||||
"parent_id": "17870412345678901",
|
||||
"timestamp": "2023-09-18T12:05:00+0000",
|
||||
}
|
||||
|
||||
result, err := parseIGCommentChangeValue(value)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "17870498765432109", result.CommentID)
|
||||
assert.Equal(t, "17870412345678901", result.ParentID)
|
||||
assert.Equal(t, "replyuser", result.From.Username)
|
||||
})
|
||||
|
||||
t.Run("deleted comment", func(t *testing.T) {
|
||||
value := map[string]interface{}{
|
||||
"id": "17870412345678901",
|
||||
"media_id": "18027993912345678",
|
||||
"is_deleted": true,
|
||||
"timestamp": "2023-09-18T12:30:00+0000",
|
||||
}
|
||||
|
||||
result, err := parseIGCommentChangeValue(value)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "17870412345678901", result.CommentID)
|
||||
assert.Equal(t, "18027993912345678", result.MediaID)
|
||||
assert.True(t, result.IsDeleted)
|
||||
})
|
||||
|
||||
t.Run("nil value returns empty struct", func(t *testing.T) {
|
||||
// parseIGCommentChangeValue marshals nil → "null" → unmarshals to empty struct
|
||||
result, err := parseIGCommentChangeValue(nil)
|
||||
// json.Marshal(nil) produces "null", json.Unmarshal("null", &struct) clears it
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "", result.CommentID)
|
||||
assert.Equal(t, "", result.MediaID)
|
||||
})
|
||||
|
||||
t.Run("missing from field", func(t *testing.T) {
|
||||
value := map[string]interface{}{
|
||||
"media_id": "18027993912345678",
|
||||
"id": "17870412345678901",
|
||||
"text": "Hello",
|
||||
}
|
||||
|
||||
result, err := parseIGCommentChangeValue(value)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "18027993912345678", result.MediaID)
|
||||
assert.Equal(t, "Hello", result.Text)
|
||||
assert.Equal(t, "", result.From.ID)
|
||||
assert.Equal(t, "", result.From.Username)
|
||||
})
|
||||
}
|
||||
|
||||
// TestWebhookParserClassifyCommentEvent tests the classifyChangeEvent method
|
||||
// for Instagram comment changes.
|
||||
func TestWebhookParserClassifyCommentEvent(t *testing.T) {
|
||||
wp := NewWebhookParser()
|
||||
|
||||
t.Run("new comment classified as EventIGComment", func(t *testing.T) {
|
||||
change := FBChangeEvent{
|
||||
Field: "comments",
|
||||
Value: map[string]interface{}{
|
||||
"from": map[string]interface{}{
|
||||
"id": "17841400123456",
|
||||
"username": " commenter1",
|
||||
},
|
||||
"media_id": "18027993912345",
|
||||
"id": "17870412345678",
|
||||
"text": "Nice!",
|
||||
},
|
||||
}
|
||||
|
||||
result := wp.classifyChangeEvent("instagram", "page123", change, 1695000000000, nil)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, EventIGComment, result.EventType)
|
||||
assert.Equal(t, "instagram", result.Object)
|
||||
assert.NotNil(t, result.Comment)
|
||||
})
|
||||
|
||||
t.Run("comment reply classified as EventIGCommentReply", func(t *testing.T) {
|
||||
change := FBChangeEvent{
|
||||
Field: "comments",
|
||||
Value: map[string]interface{}{
|
||||
"from": map[string]interface{}{
|
||||
"id": "17841400987",
|
||||
"username": "replyer",
|
||||
},
|
||||
"media_id": "18027993912345",
|
||||
"id": "17870498765432",
|
||||
"text": "Reply text",
|
||||
"parent_id": "17870412345678",
|
||||
},
|
||||
}
|
||||
|
||||
result := wp.classifyChangeEvent("instagram", "page123", change, 1695000000000, nil)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, EventIGCommentReply, result.EventType)
|
||||
assert.NotNil(t, result.Comment)
|
||||
assert.Equal(t, "17870412345678", result.Comment.ParentID)
|
||||
})
|
||||
|
||||
t.Run("deleted comment classified as EventIGCommentDeleted", func(t *testing.T) {
|
||||
change := FBChangeEvent{
|
||||
Field: "comments",
|
||||
Value: map[string]interface{}{
|
||||
"id": "17870412345678",
|
||||
"media_id": "18027993912345",
|
||||
"is_deleted": true,
|
||||
},
|
||||
}
|
||||
|
||||
result := wp.classifyChangeEvent("instagram", "page123", change, 1695000000000, nil)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, EventIGCommentDeleted, result.EventType)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user