package app import ( "context" "reflect" "testing" "time" "evening_detective_server/internal/modules/processor_jwt" "evening_detective_server/internal/services/game_model" main "evening_detective_server/proto" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) func TestGameReadAccess(t *testing.T) { for _, tc := range []struct { name string venue bool roles []string allowed, restricted bool }{ {"developer venue", true, []string{"developer"}, true, true}, {"developer prod", false, []string{"developer"}, false, false}, {"user venue", true, []string{"user"}, false, false}, {"developer organizer", true, []string{"developer", "organizer"}, true, false}, {"developer author", true, []string{"developer", "author"}, true, false}, {"admin prod", false, []string{"admin"}, true, false}, {"organizer prod", false, []string{"organizer"}, true, false}, {"author prod", false, []string{"author"}, true, false}, } { t.Run(tc.name, func(t *testing.T) { allowed, restricted := gameReadAccess(&processor_jwt.JWTClaims{Roles: tc.roles}, tc.venue) if allowed != tc.allowed || restricted != tc.restricted { t.Fatalf("access = %v, %v", allowed, restricted) } }) } } func TestGameSummaryExcludesPrivateFields(t *testing.T) { for _, location := range []*string{nil, ptrGameLocation("venue")} { t.Run("summary", func(t *testing.T) { game := &game_model.Game{ID: 7, Name: "name", Description: "description", Format: "offline", Location: location, StartAt: time.Unix(123, 0), Status: "started"} // Populate every non-summary field to catch accidental exposure when the model grows. value := reflect.ValueOf(game).Elem() for i := 0; i < value.NumField(); i++ { field := value.Field(i) if field.Kind() == reflect.Pointer && field.IsNil() && value.Type().Field(i).Name != "Location" { field.Set(reflect.New(field.Type().Elem())) } if field.Kind() == reflect.Slice { field.Set(reflect.MakeSlice(field.Type(), 1, 1)) } } want := &main.Game{ID: 7, Name: "name", Description: "description", Format: "offline", StartAt: timestamppb.New(game.StartAt), Status: "started"} if location != nil { want.Location = *location } if !proto.Equal(mapGameSummary(game), want) { t.Fatal("summary exposes private fields or loses public fields") } }) } } func ptrGameLocation(value string) *string { return &value } func TestDeveloperCannotManageGames(t *testing.T) { s := &server{} ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{Roles: []string{"developer"}}) for _, tc := range []struct { name string call func() error }{ {"add", func() error { _, err := s.AddGame(ctx, &main.AddGameReq{}); return err }}, {"update", func() error { _, err := s.UpdateGame(ctx, &main.UpdateGameReq{}); return err }}, {"delete", func() error { _, err := s.DeleteGame(ctx, &main.DeleteGameReq{}); return err }}, {"start", func() error { _, err := s.StartGame(ctx, &main.StartGameReq{}); return err }}, {"pause", func() error { _, err := s.PauseGame(ctx, &main.PauseGameReq{}); return err }}, {"play", func() error { _, err := s.PlayGame(ctx, &main.PlayGameReq{}); return err }}, {"finish", func() error { _, err := s.FinishGame(ctx, &main.FinishGameReq{}); return err }}, {"reset", func() error { _, err := s.ResetGame(ctx, &main.ResetGameReq{}); return err }}, {"answers", func() error { _, err := s.GetGameAnswers(ctx, &main.GetGameAnswersReq{}); return err }}, {"results", func() error { _, err := s.GetGameResults(ctx, &main.GetGameResultsReq{}); return err }}, } { t.Run(tc.name, func(t *testing.T) { if err := tc.call(); status.Code(err) != codes.PermissionDenied { t.Fatal(err) } }) } }