generated from VLADIMIR/template
up
This commit is contained in:
@@ -377,7 +377,7 @@ func verifyExportedGameReads(t *testing.T, f serviceFixture, worker *game_export
|
||||
games := game_service.NewGameService(f.games, f.teams, f.scenarios, nil, "", actions_repo.NewActionsRepo(f.pool), applications_repo.NewApplicationsRepo(f.pool))
|
||||
mapRepo := game_map_repo.NewGameMapRepo(f.pool)
|
||||
server := app.NewServer(nil, nil, nil, nil, games, nil, nil, nil, nil, nil, nil,
|
||||
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil)
|
||||
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil)
|
||||
ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{UserID: f.userID, Roles: []string{"organizer"}})
|
||||
game, err := server.GetGame(ctx, &proto.GetGameReq{ID: int32(f.gameID)})
|
||||
check(t, err)
|
||||
|
||||
@@ -9,6 +9,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"evening_detective_server/internal/app"
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -101,7 +107,7 @@ func (f returnFixture) pack(t *testing.T) []byte {
|
||||
}
|
||||
func (f returnFixture) apply(raw []byte) (*game_returns_repo.Return, error) {
|
||||
service := results.NewGameResultsApplyService(game_returns_repo.NewGameReturnsRepo(f.pool), f.storage, "prod", "https://game.example")
|
||||
r, err := service.Apply(context.Background(), bytes.NewReader(raw), int64(len(raw)), f.userID)
|
||||
r, err := service.Apply(context.Background(), f.gameID, bytes.NewReader(raw), f.userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w (cause: %v)", err, errors.Unwrap(err))
|
||||
}
|
||||
@@ -506,3 +512,68 @@ func TestReturnOverwritesOnlineEditsAndIsolatesScenario(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturnHTTPRealService(t *testing.T) {
|
||||
f := newReturnFixture(t, "online")
|
||||
raw := f.pack(t)
|
||||
service := results.NewGameResultsApplyService(game_returns_repo.NewGameReturnsRepo(f.pool), f.storage, "prod", "https://game.example")
|
||||
jwt := processor_jwt.NewProcessor("results-test-secret")
|
||||
temp := t.TempDir()
|
||||
t.Setenv("TMPDIR", temp)
|
||||
for _, tc := range []struct {
|
||||
name, role, mime string
|
||||
id int
|
||||
body []byte
|
||||
size int64
|
||||
want int
|
||||
}{
|
||||
{"wrong game before apply", "organizer", "application/zip", f.gameID + 1, raw, -1, 400},
|
||||
{"chunked apply", "organizer", "application/zip", f.gameID, raw, -1, 200},
|
||||
{"wrong game after apply", "organizer", "application/zip", f.gameID + 1, raw, -1, 400},
|
||||
{"admin repeat", "admin", "application/zip", f.gameID, raw, -1, 200},
|
||||
{"developer repeat", "developer", "application/zip", f.gameID, raw, -1, 200},
|
||||
{"author", "author", "application/zip", f.gameID, raw, -1, 403},
|
||||
{"missing auth", "", "application/zip", f.gameID, raw, -1, 401},
|
||||
{"invalid route", "organizer", "application/zip", 0, raw, -1, 400},
|
||||
{"MIME", "organizer", "application/json", f.gameID, raw, -1, 400},
|
||||
{"size", "organizer", "application/zip", f.gameID, raw, 1<<30 + 1, 413},
|
||||
{"invalid ZIP", "organizer", "application/zip", f.gameID, []byte("invalid"), -1, 400},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(tc.body))
|
||||
req.ContentLength = tc.size
|
||||
req.SetPathValue("id", fmt.Sprint(tc.id))
|
||||
req.Header.Set("Content-Type", tc.mime)
|
||||
if tc.role != "" {
|
||||
token, err := jwt.GenerateAccessToken(f.userID, "test@test.invalid", []string{tc.role})
|
||||
check(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
app.GameResultsApplyHandler(jwt, service).ServeHTTP(w, req)
|
||||
if w.Code != tc.want {
|
||||
t.Fatalf("HTTP %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if tc.want == 200 {
|
||||
var response map[string]any
|
||||
check(t, json.Unmarshal(w.Body.Bytes(), &response))
|
||||
if len(response) != 3 || response["gameID"] != float64(f.gameID) || response["packageID"] == nil || response["appliedAt"] == nil {
|
||||
t.Fatal("private registry exposed or bad response")
|
||||
}
|
||||
} else if strings.Contains(w.Body.String(), temp) {
|
||||
t.Fatal("private path exposed")
|
||||
}
|
||||
files, err := os.ReadDir(temp)
|
||||
check(t, err)
|
||||
if len(files) != 0 {
|
||||
t.Fatal("staging files leaked")
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, id := range []int{0, -1, f.gameID + 1} {
|
||||
_, err := service.Apply(context.Background(), id, bytes.NewReader(raw), f.userID)
|
||||
if !errors.Is(err, results.ErrInvalidPackage) {
|
||||
t.Fatal("route mismatch bypassed repeat", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
ErrReturnFailed = errors.New("не удалось применить результаты")
|
||||
)
|
||||
|
||||
// GameResultsApplyService applies private result archives; transport is added separately.
|
||||
// GameResultsApplyService applies private result archives.
|
||||
type GameResultsApplyService struct {
|
||||
repo *game_returns_repo.GameReturnsRepo
|
||||
storage file_storage.IStreamStorage
|
||||
@@ -41,17 +41,23 @@ func NewGameResultsApplyService(repo *game_returns_repo.GameReturnsRepo, storage
|
||||
}
|
||||
|
||||
// Apply preserves recorded repeats, including older packages and deleted games.
|
||||
func (s *GameResultsApplyService) Apply(ctx context.Context, input io.ReaderAt, size int64, initiatorID int) (*game_returns_repo.Return, error) {
|
||||
func (s *GameResultsApplyService) Apply(ctx context.Context, expectedGameID int, input io.Reader, initiatorID int) (*game_returns_repo.Return, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.mode != "prod" || s.repo == nil || s.storage == nil {
|
||||
return nil, ErrReturnNotAvailable
|
||||
}
|
||||
bundle, digest, err := readResultInput(ctx, input, size)
|
||||
if expectedGameID <= 0 {
|
||||
return nil, ErrInvalidPackage
|
||||
}
|
||||
bundle, digest, err := readResultInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, safeReturnError(ctx, err)
|
||||
}
|
||||
if bundle.Manifest.GameID != expectedGameID {
|
||||
return nil, ErrInvalidPackage
|
||||
}
|
||||
result, err := s.apply(ctx, bundle, digest, initiatorID)
|
||||
if err == nil || errors.Is(err, ErrSourceMismatch) || errors.Is(err, ErrReturnConflict) || errors.Is(err, ErrReturnRetry) || errors.Is(err, ErrInvalidPackage) {
|
||||
return result, err
|
||||
@@ -281,27 +287,27 @@ func safeReturnError(ctx context.Context, err error) error {
|
||||
}
|
||||
|
||||
// Stage once so the validated payload and idempotency digest always describe the same bytes.
|
||||
func readResultInput(ctx context.Context, input io.ReaderAt, size int64) (*Bundle, string, error) {
|
||||
if input == nil || size <= 0 {
|
||||
func readResultInput(ctx context.Context, input io.Reader) (*Bundle, string, error) {
|
||||
if input == nil {
|
||||
return nil, "", ErrInvalidPackage
|
||||
}
|
||||
if size > game_package_service.MaxArchiveSize {
|
||||
return nil, "", ErrLimitExceeded
|
||||
}
|
||||
file, err := os.CreateTemp("", "game-return-results-*.zip")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer func() { _ = file.Close(); _ = os.Remove(file.Name()) }()
|
||||
hash := sha256.New()
|
||||
n, err := io.Copy(io.MultiWriter(file, hash), &contextReader{ctx: ctx, reader: io.NewSectionReader(input, 0, size)})
|
||||
n, err := io.Copy(io.MultiWriter(file, hash), &contextReader{ctx: ctx, reader: io.LimitReader(input, game_package_service.MaxArchiveSize+1)})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if n != size {
|
||||
if n > game_package_service.MaxArchiveSize {
|
||||
return nil, "", ErrLimitExceeded
|
||||
}
|
||||
if n == 0 {
|
||||
return nil, "", ErrInvalidPackage
|
||||
}
|
||||
bundle, err := Unpack(ctx, file, size)
|
||||
bundle, err := Unpack(ctx, file, n)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestRepeatReturnRejectsChangedBytes(t *testing.T) {
|
||||
func TestApplyModeAndSafeErrors(t *testing.T) {
|
||||
for _, mode := range []string{"venue", "", "unknown"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
_, err := NewGameResultsApplyService(nil, nil, mode, "").Apply(context.Background(), nil, 0, 0)
|
||||
_, err := NewGameResultsApplyService(nil, nil, mode, "").Apply(context.Background(), 1, nil, 0)
|
||||
if !errors.Is(err, ErrReturnNotAvailable) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -166,12 +166,12 @@ func TestOriginalMaterialsRestoresSavedURLs(t *testing.T) {
|
||||
}
|
||||
|
||||
type inspectedResultReader struct {
|
||||
reader io.ReaderAt
|
||||
reader io.Reader
|
||||
inspect func()
|
||||
fail error
|
||||
}
|
||||
|
||||
func (r *inspectedResultReader) ReadAt(raw []byte, offset int64) (int, error) {
|
||||
func (r *inspectedResultReader) Read(raw []byte) (int, error) {
|
||||
if r.inspect != nil {
|
||||
r.inspect()
|
||||
r.inspect = nil
|
||||
@@ -179,7 +179,7 @@ func (r *inspectedResultReader) ReadAt(raw []byte, offset int64) (int, error) {
|
||||
if r.fail != nil {
|
||||
return 0, r.fail
|
||||
}
|
||||
return r.reader.ReadAt(raw, offset)
|
||||
return r.reader.Read(raw)
|
||||
}
|
||||
|
||||
func TestResultStagingIsPrivateAndAlwaysRemoved(t *testing.T) {
|
||||
@@ -230,7 +230,7 @@ func TestResultStagingIsPrivateAndAlwaysRemoved(t *testing.T) {
|
||||
}
|
||||
},
|
||||
}
|
||||
bundle, digest, err := readResultInput(ctx, reader, int64(len(raw)))
|
||||
bundle, digest, err := readResultInput(ctx, reader)
|
||||
if !errors.Is(err, tc.expected) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestProdFlowReturns(t *testing.T) {
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applied, err := service.Apply(ctx, bytes.NewReader(raw), int64(len(raw)), 0)
|
||||
applied, err := service.Apply(ctx, gameID, bytes.NewReader(raw), 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -103,14 +103,14 @@ func TestProdFlowReturns(t *testing.T) {
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newer, err := service.Apply(ctx, bytes.NewReader(corrected), int64(len(corrected)), 0)
|
||||
newer, err := service.Apply(ctx, gameID, bytes.NewReader(corrected), 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !newer.PreparedAt.After(applied.PreparedAt) {
|
||||
t.Fatal("correction did not follow original")
|
||||
}
|
||||
repeated, err := service.Apply(ctx, bytes.NewReader(raw), int64(len(raw)), 0)
|
||||
repeated, err := service.Apply(ctx, gameID, bytes.NewReader(raw), 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user