Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/models/contest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Contest < ApplicationRecord
has_many :registrations, dependent: :destroy
has_many :team_registrations, dependent: :destroy
has_many :contest_admins, dependent: :destroy
enum standings_mode: { atcoder: 1, icpc: 2 }
enum :standings_mode, { atcoder: 1, icpc: 2 }

def to_param
slug
Expand Down
2 changes: 1 addition & 1 deletion app/models/testcase_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ class TestcaseSet < ApplicationRecord
has_many :testcase_testcase_sets, dependent: :destroy
has_many :testcases, through: :testcase_testcase_sets

enum aggregate_type: { all: 0, sum: 1, max: 2, min: 3 }, _prefix: true
enum :aggregate_type, { all: 0, sum: 1, max: 2, min: 3 }, prefix: true
end
2 changes: 1 addition & 1 deletion config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
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--U680iiqw8BwSfFnB--oJCc58+Gp18padgfrw7ikA==
Dh5c0G4F2lk6QKsUnBYEkw1yuNtwBDvH4n7LVP87bwQ/cIk6gbEEhVBD5atCD5G1k0KXLZ975HRNiIhBhr81v64qJHWLntdtAjQec30JHypDKkp2NJraJ1FOHcBWXWEeOQL+nQWbM+syo1wSOkDPdO9wfwp9d0F9pTTGzmCoda6zmXOUH7+m5njFex5eSwJZN9kuWs6mo3XJohu3wxOmysHr/5m7G9QGsjYaVbLFr0N/89Q0Nibi3dI9eMYF+HCfPsBlL0HZRs1fuuH8HN7PFi/jaQ/E84gg2/TNqJJeHzyx6VRv/shHLGkopmWSAp7ocyRiTeFF2u0ZZsG4E9WOZusT6FWXuvr6xXE5Q2/VUwDG3izKbWcCj0YKUWFVXNN5vDeJcr0ZmjdjQEs3uD+2JIHWHQG1PRKlA0b/bS/D2kqcEhM+0iVfhMiSILWuUP3iWcpKLRYWWLe7dCpEEh/0czhQ03a2hrZgswPoOIWt3r5LLR67aLgoxGECTz34lyl49FP9ZTuRM/HFZVX9794egYMadE+MUkJHz/pCNqvbkFxsC5E9QPsxvjDLNRUJtyUAE9hxYggJleRT208u--98m5Tsh5nFExoxA+--ttNZU45UXnqspuaSPMQ6PQ==
5 changes: 5 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@

# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true

# Enable session middleware for DeviseTokenAuth compatibility in integration tests.
config.session_store :cache_store, key: '_mofe_back_test_session'
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CacheStore, config.session_options
end
49 changes: 26 additions & 23 deletions lib/utils/google_cloud_storage_client.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
require 'google/cloud/storage'
require "logger"

module Utils::GoogleCloudStorageClient
lgr = Logger.new $stderr
lgr.level = Logger::INFO

# Set the Google API Client logger
Google::Apis.logger = lgr

@storage = Google::Cloud::Storage.new(
project_id: Rails.application.credentials.gcs[:project_id],
credentials: {
type: "service_account",
private_key_id: Rails.application.credentials.gcs[:private_key_id],
private_key: Rails.application.credentials.gcs[:private_key],
client_email: Rails.application.credentials.gcs[:client_email],
client_id: Rails.application.credentials.gcs[:client_id],
auth_uri: "https://accounts.google.com/o/oauth2/auth",
token_uri: "https://accounts.google.com/o/oauth2/token",
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
client_x509_cert_url: Rails.application.credentials.gcs[:client_x509_cert_url]
}
)
@source_bucket = @storage.bucket('cafecoder-submit-source')
@testcase_bucket = @storage.bucket('cafecoder-testcase')
unless Rails.env.test?
require 'google/cloud/storage'

lgr = Logger.new $stderr
lgr.level = Logger::INFO

# Set the Google API Client logger
Google::Apis.logger = lgr

@storage = Google::Cloud::Storage.new(
project_id: Rails.application.credentials.gcs[:project_id],
credentials: {
type: "service_account",
private_key_id: Rails.application.credentials.gcs[:private_key_id],
private_key: Rails.application.credentials.gcs[:private_key],
client_email: Rails.application.credentials.gcs[:client_email],
client_id: Rails.application.credentials.gcs[:client_id],
auth_uri: "https://accounts.google.com/o/oauth2/auth",
token_uri: "https://accounts.google.com/o/oauth2/token",
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
client_x509_cert_url: Rails.application.credentials.gcs[:client_x509_cert_url]
}
)
@source_bucket = @storage.bucket('cafecoder-submit-source')
@testcase_bucket = @storage.bucket('cafecoder-testcase')
end

def self.upload_source(file_name, file_content)
@source_bucket.create_file(StringIO.new(file_content), file_name)
Expand Down
311 changes: 311 additions & 0 deletions test/controllers/api/submissions_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
require 'test_helper'

class Api::SubmissionsControllerTest < ActionDispatch::IntegrationTest
setup do
@ended_contest = contests(:ended_contest)
@running_contest = contests(:running_contest)
@ended_problem = problems(:ended_problem)
@running_problem = problems(:running_problem)
@public_submission = submissions(:public_submission)
@private_submission = submissions(:private_submission)
@other_user_submission = submissions(:other_user_submission)
@running_contest_submission = submissions(:running_contest_submission)
@admin_user = users(:admin_user)
@writer_user = users(:writer_user)
@general_user = users(:general_user)
@other_user = users(:other_user)

# Stub GCS get_source for show tests to avoid external dependency
Utils::GoogleCloudStorageClient.define_singleton_method(:get_source) do |_file_name|
StringIO.new("dummy source code")
end
end

teardown do
Utils::GoogleCloudStorageClient.singleton_class.remove_method(:get_source)
end

private

def auth_headers(user)
user.create_new_auth_token
end

# ============================================
# GET /api/contests/:contest_slug/submissions
# (index - user's own submissions)
# ============================================

public

test "index: returns unauthorized when not signed in" do
get api_contest_submissions_url(contest_slug: @ended_contest.slug), as: :json
assert_response :unauthorized
end

test "index: returns own submissions for ended contest" do
get api_contest_submissions_url(contest_slug: @ended_contest.slug),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert json.key?('data')
assert json.key?('meta')

data = json['data']
# general_user has public_submission and private_submission on ended_problem
assert_equal 2, data.length
data.each do |submission|
assert_equal @general_user.name, submission['user']['name']
end
end

test "index: returns only own submissions, not other users'" do
get api_contest_submissions_url(contest_slug: @ended_contest.slug),
headers: auth_headers(@other_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
data = json['data']
assert_equal 1, data.length
assert_equal @other_user.name, data[0]['user']['name']
end

test "index: returns submissions for running contest" do
get api_contest_submissions_url(contest_slug: @running_contest.slug),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
data = json['data']
assert_equal 1, data.length
end

test "index: returns empty list when user has no submissions" do
get api_contest_submissions_url(contest_slug: @ended_contest.slug),
headers: auth_headers(@admin_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert_equal 0, json['data'].length
end

test "index: returns not found for non-existent contest" do
get api_contest_submissions_url(contest_slug: 'nonexistent'),
headers: auth_headers(@general_user),
as: :json
assert_response :not_found
end

test "index: includes pagination metadata" do
get api_contest_submissions_url(contest_slug: @ended_contest.slug),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
meta = json['meta']
assert meta.key?('pagination')
pagination = meta['pagination']
assert pagination.key?('current')
assert pagination.key?('pages')
assert pagination.key?('count')
end

test "index: submission data includes expected fields" do
get api_contest_submissions_url(contest_slug: @ended_contest.slug),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
submission = json['data'].first
assert submission.key?('id')
assert submission.key?('user')
assert submission.key?('task')
assert submission.key?('status')
assert submission.key?('point')
assert submission.key?('execution_time')
assert submission.key?('lang')
assert submission.key?('timestamp')
end

# ============================================
# GET /api/contests/:contest_slug/submissions/all
# (all submissions with visibility rules)
# ============================================

test "all: returns submissions for ended contest when not signed in" do
get all_api_contest_submissions_url(contest_slug: @ended_contest.slug), as: :json
assert_response :success
json = JSON.parse(response.body)
data = json['data']
# Only public submissions are visible to anonymous users
data.each do |submission|
assert_equal true, submission['public']
end
end

test "all: returns forbidden for running contest when not signed in" do
get all_api_contest_submissions_url(contest_slug: @running_contest.slug), as: :json
assert_response :forbidden
end

test "all: returns forbidden for running contest for regular user" do
get all_api_contest_submissions_url(contest_slug: @running_contest.slug),
headers: auth_headers(@general_user),
as: :json
assert_response :forbidden
end

test "all: returns submissions for ended contest when signed in" do
get all_api_contest_submissions_url(contest_slug: @ended_contest.slug),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
data = json['data']
# Signed-in user can see public submissions + own submissions
assert data.length >= 1
end

test "all: admin can see all submissions for running contest" do
get all_api_contest_submissions_url(contest_slug: @running_contest.slug),
headers: auth_headers(@admin_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert json.key?('data')
end

test "all: writer can see submissions for running contest" do
get all_api_contest_submissions_url(contest_slug: @running_contest.slug),
headers: auth_headers(@writer_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert json.key?('data')
end

test "all: returns not found for non-existent contest" do
get all_api_contest_submissions_url(contest_slug: 'nonexistent'), as: :json
assert_response :not_found
end

test "all: includes pagination metadata" do
get all_api_contest_submissions_url(contest_slug: @ended_contest.slug),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert json['meta'].key?('pagination')
end

# ============================================
# GET /api/contests/:contest_slug/submissions/:id
# (show - submission detail)
# ============================================

test "show: owner can view own public submission in ended contest" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @public_submission.id),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert_equal @public_submission.id, json['id']
end

test "show: owner can view own private submission in ended contest" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @private_submission.id),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert_equal @private_submission.id, json['id']
end

test "show: anonymous user can view public submission in ended contest" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @public_submission.id),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert_equal @public_submission.id, json['id']
end

test "show: anonymous user cannot view private submission in ended contest" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @private_submission.id),
as: :json
assert_response :forbidden
end

test "show: other user cannot view private submission in ended contest" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @private_submission.id),
headers: auth_headers(@other_user),
as: :json
assert_response :forbidden
end

test "show: admin can view any submission" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @private_submission.id),
headers: auth_headers(@admin_user),
as: :json
assert_response :success
end

test "show: writer of the problem can view any submission on that problem" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @private_submission.id),
headers: auth_headers(@writer_user),
as: :json
assert_response :success
end

test "show: returns not found when contest_slug does not match submission's contest" do
get api_contest_submission_url(contest_slug: @running_contest.slug, id: @public_submission.id),
headers: auth_headers(@general_user),
as: :json
assert_response :not_found
end

test "show: returns not found for non-existent submission" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: 999999),
headers: auth_headers(@general_user),
as: :json
assert_response :not_found
end

test "show: anonymous user cannot view submission in running contest" do
get api_contest_submission_url(contest_slug: @running_contest.slug, id: @running_contest_submission.id),
as: :json
assert_response :forbidden
end

test "show: non-owner cannot view submission in running contest" do
get api_contest_submission_url(contest_slug: @running_contest.slug, id: @running_contest_submission.id),
headers: auth_headers(@other_user),
as: :json
assert_response :forbidden
end

test "show: owner can view own submission in running contest" do
get api_contest_submission_url(contest_slug: @running_contest.slug, id: @running_contest_submission.id),
headers: auth_headers(@general_user),
as: :json
assert_response :success
end

test "show: response includes expected detail fields" do
get api_contest_submission_url(contest_slug: @ended_contest.slug, id: @public_submission.id),
headers: auth_headers(@general_user),
as: :json
assert_response :success
json = JSON.parse(response.body)
assert json.key?('id')
assert json.key?('user')
assert json.key?('task')
assert json.key?('status')
assert json.key?('point')
assert json.key?('execution_time')
assert json.key?('lang')
assert json.key?('compile_error')
assert json.key?('testcase_results')
assert json.key?('testcase_sets')
end
end
Loading