package game import ( "context" "errors" "testing" "albatross-2026-backend/db" "albatross-2026-backend/taskqueue" ) // mockTaskQueue implements TaskQueueInterface for testing. type mockTaskQueue struct { enqueued []taskqueue.TaskPayloadRunTestcase err error } func (m *mockTaskQueue) EnqueueTaskRunTestcase(gameID, userID, submissionID, testcaseID int, language, code, stdin, stdout string) error { if m.err != nil { return m.err } m.enqueued = append(m.enqueued, taskqueue.TaskPayloadRunTestcase{ GameID: gameID, UserID: userID, SubmissionID: submissionID, TestcaseID: testcaseID, Language: language, Code: code, Stdin: stdin, Stdout: stdout, }) return nil } // mockQuerier implements db.Querier for testing. type mockQuerier struct { db.Querier listTestcasesByGameIDFunc func(ctx context.Context, gameID int32) ([]db.Testcase, error) createTestcaseResultFunc func(ctx context.Context, arg db.CreateTestcaseResultParams) error createTestcaseResultCalls []db.CreateTestcaseResultParams } func (m *mockQuerier) ListTestcasesByGameID(ctx context.Context, gameID int32) ([]db.Testcase, error) { if m.listTestcasesByGameIDFunc != nil { return m.listTestcasesByGameIDFunc(ctx, gameID) } return nil, nil } func (m *mockQuerier) CreateTestcaseResult(_ context.Context, arg db.CreateTestcaseResultParams) error { m.createTestcaseResultCalls = append(m.createTestcaseResultCalls, arg) if m.createTestcaseResultFunc != nil { return m.createTestcaseResultFunc(context.Background(), arg) } return nil } func TestEnqueueTestTasks(t *testing.T) { testcases := []db.Testcase{ {TestcaseID: 1, ProblemID: 10, Stdin: "input1", Stdout: "output1"}, {TestcaseID: 2, ProblemID: 10, Stdin: "input2", Stdout: "output2"}, } tq := &mockTaskQueue{} mq := &mockQuerier{ listTestcasesByGameIDFunc: func(_ context.Context, _ int32) ([]db.Testcase, error) { return testcases, nil }, } hub := &Hub{q: mq, taskQueue: tq, ctx: context.Background()} err := hub.EnqueueTestTasks(context.Background(), 100, 1, 42, "php", "", language: "php", want: 6, // "echo1;" after stripping whitespace, "" }, { name: "php with whitespace", code: "", language: "php", want: 6, }, { name: "non-php language", code: "print(1)", language: "swift", want: 8, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := hub.CalcCodeSize(tt.code, tt.language) if got != tt.want { t.Errorf("CalcCodeSize(%q, %q) = %d, want %d", tt.code, tt.language, got, tt.want) } }) } } // mockTxManager implements db.TxManager for testing. type mockTxManager struct { err error } func (m *mockTxManager) RunInTx(_ context.Context, fn func(q db.Querier) error) error { if m.err != nil { return m.err } return fn(&mockTxQuerier{}) } // mockTxQuerier is a Querier returned inside RunInTx, recording calls. type mockTxQuerier struct { db.Querier updateSubmissionStatusCalled bool updateGameStateStatusCalled bool syncGameStateBestScoreSubmissionCalled bool } func (m *mockTxQuerier) UpdateSubmissionStatus(_ context.Context, _ db.UpdateSubmissionStatusParams) error { m.updateSubmissionStatusCalled = true return nil } func (m *mockTxQuerier) UpdateGameStateStatus(_ context.Context, _ db.UpdateGameStateStatusParams) error { m.updateGameStateStatusCalled = true return nil } func (m *mockTxQuerier) SyncGameStateBestScoreSubmission(_ context.Context, _ db.SyncGameStateBestScoreSubmissionParams) error { m.syncGameStateBestScoreSubmissionCalled = true return nil } // recordingTxManager tracks what fn does with the Querier. type recordingTxManager struct { lastQuerier *mockTxQuerier } func (m *recordingTxManager) RunInTx(_ context.Context, fn func(q db.Querier) error) error { q := &mockTxQuerier{} m.lastQuerier = q return fn(q) } func TestUpdateSubmissionAndGameState_Success(t *testing.T) { txm := &recordingTxManager{} hub := &Hub{ q: &mockQuerier{}, txm: txm, ctx: context.Background(), } result := &taskqueue.TaskResultRunTestcase{ TaskPayload: &taskqueue.TaskPayloadRunTestcase{ GameID: 1, UserID: 2, SubmissionID: 3, }, } err := hub.updateSubmissionAndGameState(result, "success") if err != nil { t.Fatalf("unexpected error: %v", err) } if !txm.lastQuerier.updateSubmissionStatusCalled { t.Error("expected UpdateSubmissionStatus to be called") } if !txm.lastQuerier.updateGameStateStatusCalled { t.Error("expected UpdateGameStateStatus to be called") } if !txm.lastQuerier.syncGameStateBestScoreSubmissionCalled { t.Error("expected SyncGameStateBestScoreSubmission to be called for 'success' status") } } func TestUpdateSubmissionAndGameState_Failure(t *testing.T) { txm := &recordingTxManager{} hub := &Hub{ q: &mockQuerier{}, txm: txm, ctx: context.Background(), } result := &taskqueue.TaskResultRunTestcase{ TaskPayload: &taskqueue.TaskPayloadRunTestcase{ GameID: 1, UserID: 2, SubmissionID: 3, }, } err := hub.updateSubmissionAndGameState(result, "wrong_answer") if err != nil { t.Fatalf("unexpected error: %v", err) } if !txm.lastQuerier.updateSubmissionStatusCalled { t.Error("expected UpdateSubmissionStatus to be called") } if !txm.lastQuerier.updateGameStateStatusCalled { t.Error("expected UpdateGameStateStatus to be called") } if txm.lastQuerier.syncGameStateBestScoreSubmissionCalled { t.Error("expected SyncGameStateBestScoreSubmission NOT to be called for 'wrong_answer' status") } } func TestUpdateSubmissionAndGameState_TxError(t *testing.T) { txErr := errors.New("tx failed") txm := &mockTxManager{err: txErr} hub := &Hub{ q: &mockQuerier{}, txm: txm, ctx: context.Background(), } result := &taskqueue.TaskResultRunTestcase{ TaskPayload: &taskqueue.TaskPayloadRunTestcase{ GameID: 1, UserID: 2, SubmissionID: 3, }, } err := hub.updateSubmissionAndGameState(result, "success") if !errors.Is(err, txErr) { t.Errorf("expected tx error, got: %v", err) } } func TestIsTestcaseResultCorrect(t *testing.T) { tests := []struct { name string expected string actual string want bool }{ { name: "exact match", expected: "hello", actual: "hello", want: true, }, { name: "trailing newline ignored", expected: "hello\n", actual: "hello", want: true, }, { name: "CRLF normalized", expected: "hello\r\n", actual: "hello\n", want: true, }, { name: "mismatch", expected: "hello", actual: "world", want: false, }, { name: "multiline match", expected: "line1\nline2", actual: "line1\nline2\n", want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isTestcaseResultCorrect(tt.expected, tt.actual) if got != tt.want { t.Errorf("isTestcaseResultCorrect(%q, %q) = %v, want %v", tt.expected, tt.actual, got, tt.want) } }) } }