Submit
++ + + +
+ + + + + + + + +
+
diff --git a/crawler/common_test.go b/crawler/common_test.go index 8d9555c..259bc7a 100644 --- a/crawler/common_test.go +++ b/crawler/common_test.go @@ -96,15 +96,29 @@ func mockRequestClient() (*http.Client, captureFunc) { } type mockHTTPResponse struct { - status int - bodyFile string - timeout bool + lastRequestURL string + status int + bodyFile string + timeout bool +} + +func (m *mockHTTPResponse) NewClient(t *testing.T, hm htmlMap) *http.Client { + t.Helper() + rt := &mockResponseRoundTripper{status: m.status, body: hm.Get(m.bodyFile), timeout: m.timeout} + if m.lastRequestURL != "" { + req, err := http.NewRequest(http.MethodGet, m.lastRequestURL, nil) + require.NoError(t, err) + require.NotNil(t, req) + rt.lastRequest = req + } + return &http.Client{Transport: rt} } type mockResponseRoundTripper struct { - status int - body string - timeout bool + lastRequest *http.Request + status int + body string + timeout bool } func (m *mockResponseRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { @@ -114,13 +128,18 @@ func (m *mockResponseRoundTripper) RoundTrip(req *http.Request) (*http.Response, } else { wait = time.After(0 * time.Second) } + ctx := req.Context() + if m.lastRequest != nil { + req = m.lastRequest + } select { case <-wait: return &http.Response{ + Request: req, StatusCode: m.status, Body: io.NopCloser(strings.NewReader(m.body)), }, nil - case <-req.Context().Done(): + case <-ctx.Done(): return nil, errors.New("request canceled for timeout") } } diff --git a/crawler/submit.go b/crawler/submit.go index 7adea20..f0f1d27 100644 --- a/crawler/submit.go +++ b/crawler/submit.go @@ -20,7 +20,10 @@ func NewSubmit(client *http.Client) *Submit { return &Submit{crawler: NewCrawler(url.SubmitPath).WithClient(client)} } -func (c *Submit) Do(ctx context.Context, req *requests.Submit) (*responses.Submit, error) { +func (c *Submit) Do(ctx context.Context, req requests.Submit) (*responses.Submit, error) { + if err := req.Validate(); err != nil { + return nil, err + } logger := logs.FromContext(ctx) crawler := c.crawler.WithPathParam("contestID", req.ContestID) resp, err := crawler.Post(ctx, nil, req) @@ -29,6 +32,10 @@ func (c *Submit) Do(ctx context.Context, req *requests.Submit) (*responses.Submi return nil, errors.New("failed to post document") } defer resp.Body.Close() + if err := c.crawler.validHTML(ctx, resp.Body); err != nil { + logger.Error(err.Error()) + return nil, errors.New("response is not a valid HTML") + } if resp.StatusCode != http.StatusOK { logger.With("statusCode", resp.StatusCode).Error("unexpected status code") return nil, errors.New("unexpected status code for submit") diff --git a/crawler/submit_test.go b/crawler/submit_test.go new file mode 100644 index 0000000..3167bf3 --- /dev/null +++ b/crawler/submit_test.go @@ -0,0 +1,157 @@ +package crawler_test + +import ( + "context" + "net/http" + gourl "net/url" + "testing" + "time" + + "github.com/meian/atgo/constant" + "github.com/meian/atgo/crawler" + "github.com/meian/atgo/crawler/requests" + "github.com/meian/atgo/crawler/responses" + "github.com/meian/atgo/url" + "github.com/stretchr/testify/assert" +) + +func TestSubmit_Do_Request(t *testing.T) { + tests := []struct { + name string + req requests.Submit + want requestWant + }{ + { + name: "success", + req: requests.Submit{ + ContestID: "abc123", + TaskID: "abc123_c", + CSRFToken: "token", + LanguageID: constant.LanguageGo_1_20_6, + SourceCode: "package main\n\nsome code...", + }, + want: requestWant{ + path: "/contests/abc123/submit", + query: gourl.Values{}, + body: gourl.Values{ + "csrf_token": []string{"token"}, + "data.LanguageId": []string{constant.LanguageGo_1_20_6.StringValue()}, + "data.TaskScreenName": []string{"abc123_c"}, + "sourceCode": []string{"package main\n\nsome code..."}, + }, + called: true, + }, + }, + { + name: "request is invalid", + req: requests.Submit{ + ContestID: "abc123", + }, + want: requestWant{called: false}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + client, cFunc := mockRequestClient() + _, _ = crawler.NewSubmit(client).Do(context.Background(), tt.req) + method, path, query, body, called := cFunc() + if !tt.want.called { + assert.False(called) + return + } + assert.Equal(http.MethodPost, method) + assert.Equal(tt.want.path, path) + assert.Equal(tt.want.query, query) + assert.Equal(tt.want.body, body) + assert.True(called) + }) + } +} + +func TestSubmit_Do_Response(t *testing.T) { + m := testHTMLMap(t, "submit") + + type want struct { + err bool + res *responses.Submit + } + tests := []struct { + name string + httpRes mockHTTPResponse + want want + }{ + { + name: "success", + httpRes: mockHTTPResponse{ + lastRequestURL: url.MySubmissionURL("abc123"), + status: http.StatusOK, + bodyFile: "success.html", + }, + want: want{ + res: &responses.Submit{ + Submitted: true, + }, + }, + }, + { + name: "invalid parameters", + httpRes: mockHTTPResponse{ + lastRequestURL: url.SubmitURL("abc123"), + status: http.StatusOK, + bodyFile: "invalid-parameters.html", + }, + want: want{err: true}, + }, + { + name: "not found", + httpRes: mockHTTPResponse{ + lastRequestURL: url.SubmitURL("abc123"), + status: http.StatusNotFound, + }, + want: want{err: true}, + }, + { + name: "not a html response", + httpRes: mockHTTPResponse{ + lastRequestURL: url.SubmitURL("abc123"), + status: http.StatusOK, + bodyFile: "not-a-html", + }, + want: want{err: true}, + }, + { + name: "timeout", + httpRes: mockHTTPResponse{timeout: true}, + want: want{err: true}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + client := tt.httpRes.NewClient(t, m) + req := requests.Submit{ + ContestID: "abc123", + TaskID: "abc123_c", + CSRFToken: "token", + LanguageID: constant.LanguageGo_1_20_6, + SourceCode: "package main\n\nsome code...", + } + res, err := crawler.NewSubmit(client).Do(ctx, req) + if tt.want.err { + if assert.Error(err) { + t.Logf("error: %v", err) + } + return + } + assert.NoError(err) + if !assert.NotNil(res) { + return + } + assert.Equal(tt.want.res, res) + }) + } +} diff --git a/crawler/testdata/submit/invalid-parameters.html b/crawler/testdata/submit/invalid-parameters.html new file mode 100644 index 0000000..ff2638d --- /dev/null +++ b/crawler/testdata/submit/invalid-parameters.html @@ -0,0 +1,1727 @@ + + + + + + + + +
+Submission Time | +Task | +User | +Language | +Score | +Code Size | +Status | +Exec Time | +Memory | ++ |
---|---|---|---|---|---|---|---|---|---|
+ | A - Zero Sum Game | +kitamin | +Go (go 1.20.6) | +0 | +424 Byte | +WJ | ++ Detail + | +||
+ | A - Zero Sum Game | +kitamin | +Go (go 1.20.6) | +100 | +424 Byte | +AC | 1 ms | 1712 KB | ++ Detail + | +
+ | A - Zero Sum Game | +kitamin | +Go (go 1.20.6) | +100 | +424 Byte | +AC | 2 ms | 1708 KB | ++ Detail + | +
+ | A - Zero Sum Game | +kitamin | +Go (go 1.20.6) | +100 | +424 Byte | +AC | 1 ms | 1712 KB | ++ Detail + | +