feat: notification service

This commit is contained in:
Ilia Denisov
2026-04-22 08:49:45 +02:00
committed by GitHub
parent 5b7593e6f6
commit 32dc29359a
135 changed files with 21828 additions and 130 deletions
+161
View File
@@ -0,0 +1,161 @@
package notification
import (
"os"
"path/filepath"
"regexp"
"sort"
"testing"
"github.com/stretchr/testify/require"
)
const expectedPushPayloadMappingTable = `| ` + "`notification_type`" + ` | FlatBuffers table | Payload fields |
| --- | --- | --- |
| ` + "`game.turn.ready`" + ` | ` + "`notification.GameTurnReadyEvent`" + ` | ` + "`game_id`" + `, ` + "`turn_number`" + ` |
| ` + "`game.finished`" + ` | ` + "`notification.GameFinishedEvent`" + ` | ` + "`game_id`" + `, ` + "`final_turn_number`" + ` |
| ` + "`lobby.application.submitted`" + ` | ` + "`notification.LobbyApplicationSubmittedEvent`" + ` | ` + "`game_id`" + `, ` + "`applicant_user_id`" + ` |
| ` + "`lobby.membership.approved`" + ` | ` + "`notification.LobbyMembershipApprovedEvent`" + ` | ` + "`game_id`" + ` |
| ` + "`lobby.membership.rejected`" + ` | ` + "`notification.LobbyMembershipRejectedEvent`" + ` | ` + "`game_id`" + ` |
| ` + "`lobby.invite.created`" + ` | ` + "`notification.LobbyInviteCreatedEvent`" + ` | ` + "`game_id`" + `, ` + "`inviter_user_id`" + ` |
| ` + "`lobby.invite.redeemed`" + ` | ` + "`notification.LobbyInviteRedeemedEvent`" + ` | ` + "`game_id`" + `, ` + "`invitee_user_id`" + ` |`
var expectedPushPayloadSchemaTableNames = []string{
"GameTurnReadyEvent",
"GameFinishedEvent",
"LobbyApplicationSubmittedEvent",
"LobbyMembershipApprovedEvent",
"LobbyMembershipRejectedEvent",
"LobbyInviteCreatedEvent",
"LobbyInviteRedeemedEvent",
}
var expectedPushPayloadSchemaFields = map[string][]string{
"GameTurnReadyEvent": {
"game_id:string;",
"turn_number:int64;",
},
"GameFinishedEvent": {
"game_id:string;",
"final_turn_number:int64;",
},
"LobbyApplicationSubmittedEvent": {
"game_id:string;",
"applicant_user_id:string;",
},
"LobbyMembershipApprovedEvent": {
"game_id:string;",
},
"LobbyMembershipRejectedEvent": {
"game_id:string;",
},
"LobbyInviteCreatedEvent": {
"game_id:string;",
"inviter_user_id:string;",
},
"LobbyInviteRedeemedEvent": {
"game_id:string;",
"invitee_user_id:string;",
},
}
var expectedPushPayloadGeneratedFiles = []string{
"GameFinishedEvent.go",
"GameTurnReadyEvent.go",
"LobbyApplicationSubmittedEvent.go",
"LobbyInviteCreatedEvent.go",
"LobbyInviteRedeemedEvent.go",
"LobbyMembershipApprovedEvent.go",
"LobbyMembershipRejectedEvent.go",
}
var expectedPushPayloadDocumentationSnippets = []string{
"Only the seven user-facing push notification types above are represented in `notification.fbs`.",
"`geo.review_recommended`, `game.generation_failed`, `lobby.runtime_paused_after_start`, and `lobby.invite.expired` remain outside this schema because they are email-only in v1.",
"`notification_type` alone determines the concrete FlatBuffers table.",
"No extra envelope or FlatBuffers `union` is added in v1.",
"The push payload must stay lightweight and must not attempt to mirror full game, lobby, or profile state.",
"`game_name`, human-readable user names, and other full business-state fields stay out of the push schema.",
}
func TestNotificationPushPayloadSchemaFreezesTablesAndFields(t *testing.T) {
t.Parallel()
schema := loadTextFile(t, filepath.Join("..", "pkg", "schema", "fbs", "notification.fbs"))
require.Contains(t, schema, "namespace notification;")
require.Contains(t, schema, "root_type GameTurnReadyEvent;")
require.NotContains(t, schema, "union ")
tablePattern := regexp.MustCompile(`(?m)^table ([A-Za-z0-9_]+) \{$`)
matches := tablePattern.FindAllStringSubmatch(schema, -1)
actualTableNames := make([]string, 0, len(matches))
for _, match := range matches {
actualTableNames = append(actualTableNames, match[1])
}
require.Equal(t, expectedPushPayloadSchemaTableNames, actualTableNames)
for _, tableName := range expectedPushPayloadSchemaTableNames {
tableBody := extractFlatBuffersTableBody(t, schema, tableName)
for _, field := range expectedPushPayloadSchemaFields[tableName] {
require.Contains(t, tableBody, field)
}
}
}
func TestNotificationPushPayloadGeneratedBindingsStayInSync(t *testing.T) {
t.Parallel()
dirPath := filepath.Join(moduleRoot(t), "..", "pkg", "schema", "fbs", "notification")
entries, err := os.ReadDir(dirPath)
require.NoError(t, err)
actualFiles := make([]string, 0, len(entries))
for _, entry := range entries {
require.Falsef(t, entry.IsDir(), "unexpected directory in generated bindings: %s", entry.Name())
actualFiles = append(actualFiles, entry.Name())
fileContents := loadTextFile(t, filepath.Join("..", "pkg", "schema", "fbs", "notification", entry.Name()))
require.Contains(t, fileContents, "// Code generated by the FlatBuffers compiler. DO NOT EDIT.")
require.Contains(t, fileContents, "package notification")
}
sort.Strings(actualFiles)
require.Equal(t, expectedPushPayloadGeneratedFiles, actualFiles)
}
func TestNotificationPushPayloadDocsStayInSync(t *testing.T) {
t.Parallel()
readme := loadTextFile(t, "README.md")
flowsDoc := loadTextFile(t, filepath.Join("docs", "flows.md"))
examplesDoc := loadTextFile(t, filepath.Join("docs", "examples.md"))
docsIndex := loadTextFile(t, filepath.Join("docs", "README.md"))
normalizedReadme := normalizeWhitespace(readme)
normalizedFlowsDoc := normalizeWhitespace(flowsDoc)
normalizedExamplesDoc := normalizeWhitespace(examplesDoc)
require.Contains(t, readme, expectedPushPayloadMappingTable)
require.Contains(t, docsIndex, "- [Main flows](flows.md)")
require.Contains(t, docsIndex, "- [Configuration and contract examples](examples.md)")
for _, snippet := range expectedPushPayloadDocumentationSnippets {
normalizedSnippet := normalizeWhitespace(snippet)
require.Contains(t, normalizedReadme, normalizedSnippet)
}
require.Contains(t, normalizedFlowsDoc, normalizeWhitespace("encode FlatBuffers notification payload"))
require.Contains(t, normalizedExamplesDoc, normalizeWhitespace("payload_bytes '<flatbuffers-bytes>'"))
}
func extractFlatBuffersTableBody(t *testing.T, schema, tableName string) string {
t.Helper()
pattern := regexp.MustCompile(`(?s)table ` + regexp.QuoteMeta(tableName) + ` \{(.*?)\}`)
match := pattern.FindStringSubmatch(schema)
if match == nil {
require.FailNowf(t, "test failed", "table %s not found in schema", tableName)
}
return match[1]
}