├── README.md
├── Makefile
├── .gitignore
├── utils
├── cookiejar
│ ├── jar_loader.go
│ ├── punycode.go
│ ├── punycode_test.go
│ ├── jar.go
│ └── jar_test.go
└── http.go
├── LICENSE
├── hirose
├── status.go
├── login.go
├── status_test.go
├── position.go
├── order.go
├── position_test.go
└── order_test.go
└── cli
└── main.go
/README.md:
--------------------------------------------------------------------------------
1 | # fxtools
2 | [Private] 為替取引用ツール群
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | make format
3 | make build
4 |
5 | format:
6 | go fmt ./...
7 |
8 | build:
9 | go build cli/main.go
10 |
11 | test:
12 | go test ./...
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
26 | .DS_Store
27 | main
28 |
--------------------------------------------------------------------------------
/utils/cookiejar/jar_loader.go:
--------------------------------------------------------------------------------
1 | package cookiejar
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "os"
7 | )
8 |
9 | func (j *Jar) Load(filename string) error {
10 | buf, err := ioutil.ReadFile(filename)
11 | if err != nil {
12 | return err
13 | }
14 |
15 | return json.Unmarshal(buf, j)
16 | }
17 |
18 | func (j *Jar) Save(filename string) error {
19 | buf, err := json.Marshal(j)
20 | if err != nil {
21 | return err
22 | }
23 |
24 | return ioutil.WriteFile(filename, buf, os.ModePerm)
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Kentaro IMAJO
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/hirose/status.go:
--------------------------------------------------------------------------------
1 | package hirose
2 |
3 | import (
4 | "fmt"
5 | "github.com/PuerkitoBio/goquery"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | type Status struct {
11 | ActualDeposit *int64 `json:"actual_deposit"`
12 | NecessaryDeposit *int64 `json:"necessary_deposit"`
13 | }
14 |
15 | func parsePrice(str string) *int64 {
16 | val, err := strconv.ParseInt(strings.Replace(str, ",", "", -1), 10, 64)
17 | if err != nil {
18 | return nil
19 | }
20 | return &val
21 | }
22 |
23 | func parseStatusPage(s *goquery.Document) (*Status, error) {
24 | c := s.Find(".container").First()
25 | t := c.Find("div").First().Text()
26 | if t != ">証拠金状況照会<" {
27 | return nil, fmt.Errorf("cannot open \"証拠金状況照会\", but %#v", t)
28 | }
29 |
30 | results := []string{}
31 | c.Find("hr").First().NextUntil("hr").Each(
32 | func(_ int, s *goquery.Selection) {
33 | results = append(results, s.Text())
34 | })
35 |
36 | orig := map[string]string{}
37 | for i := 0; i+1 < len(results); i += 2 {
38 | k := strings.TrimSpace(strings.Replace(results[i], ":", "", -1))
39 | v := strings.TrimSpace(results[i+1])
40 | orig[k] = v
41 | }
42 |
43 | return &Status{
44 | ActualDeposit: parsePrice(orig["有効証拠金額"]),
45 | NecessaryDeposit: parsePrice(orig["必要証拠金額"]),
46 | }, nil
47 | }
48 |
49 | func (c *HiroseClient) GetStatus() (*Status, error) {
50 | doc, err := c.Fetch("GET", "/common/I103.html")
51 | if err != nil {
52 | return nil, err
53 | }
54 | return parseStatusPage(doc)
55 | }
56 |
--------------------------------------------------------------------------------
/hirose/login.go:
--------------------------------------------------------------------------------
1 | package hirose
2 |
3 | import (
4 | "../utils"
5 | "github.com/PuerkitoBio/goquery"
6 | "log"
7 | )
8 |
9 | const HOST string = "https://lionfx-mob.hirose-fx.co.jp"
10 |
11 | type HiroseClient struct {
12 | client *utils.HttpClient
13 | }
14 |
15 | func (c *HiroseClient) FetchWithQuery(method, urlStr string, query map[string]string) (*goquery.Document, error) {
16 | if c.client == nil {
17 | c.client = &utils.HttpClient{
18 | UseSjis: true,
19 | }
20 | }
21 | err := c.client.Load()
22 | if err != nil {
23 | log.Printf("Failed to load Cookie: %s", err)
24 | }
25 | doc, err := c.client.FetchDocument(method, HOST+urlStr, query)
26 | if err != nil {
27 | return nil, err
28 | }
29 | c.client.Save()
30 | return doc, err
31 | }
32 |
33 | func (c *HiroseClient) Fetch(method, urlStr string) (*goquery.Document, error) {
34 | return c.FetchWithQuery(method, urlStr, map[string]string{})
35 | }
36 |
37 | func (c *HiroseClient) SignIn(userId, password string) (bool, error) {
38 | // NOTE: HiroseFX site requires Cookie before signing in.
39 | doc, err := c.Fetch("GET", "/L001.html")
40 | if err != nil {
41 | return false, err
42 | }
43 |
44 | doc, err = c.FetchWithQuery("POST", "/L002.html", map[string]string{"user_id": userId, "password": password})
45 | if err != nil {
46 | return false, err
47 | }
48 | return doc.Find(".container>div").First().Text() == ">>メインメニュー<<", nil
49 | }
50 |
51 | func (c *HiroseClient) IsSignedIn() (bool, error) {
52 | doc, err := c.Fetch("GET", "/M001.html")
53 | if err != nil {
54 | return false, err
55 | }
56 | return doc.Find(".container>div").First().Text() == ">>メインメニュー<<", nil
57 | }
58 |
--------------------------------------------------------------------------------
/cli/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "../hirose"
5 | "encoding/json"
6 | "flag"
7 | "log"
8 | "os"
9 | // utils "../utils"
10 | "fmt"
11 | )
12 |
13 | func Command(result interface{}, err error) {
14 | if err != nil {
15 | log.Fatal(err)
16 | }
17 | buf, err := json.Marshal(result)
18 | if err != nil {
19 | log.Fatal("failed to marshal: %v", err)
20 | }
21 | fmt.Println(string(buf))
22 | os.Exit(0)
23 | }
24 |
25 | func main() {
26 | // c := utils.HttpClient{}
27 | // c.Load("/tmp/cookie")
28 | // resp, err := c.Do("GET", "https://lionfx-mob.hirose-fx.co.jp/L001.html", map[string]string{})
29 | // if err != nil {
30 | // panic(err)
31 | // }
32 | // fmt.Printf("%s", resp)
33 | // c.Save("/tmp/cookie")
34 | flag.Parse()
35 | h := hirose.HiroseClient{}
36 | command := flag.Args()[0]
37 | if command == "signin" {
38 | userId := os.Getenv("FX_USER_ID")
39 | if userId == "" {
40 | log.Fatalf("FX_USER_ID is required.")
41 | }
42 | password := os.Getenv("FX_PASSWORD")
43 | if password == "" {
44 | log.Fatalf("FX_PASSWORD is required.")
45 | }
46 |
47 | result, err := h.SignIn(userId, password)
48 | if err != nil {
49 | panic(err)
50 | }
51 | if !result {
52 | os.Exit(1)
53 | }
54 |
55 | result, err = h.IsSignedIn()
56 | if err != nil {
57 | panic(err)
58 | }
59 | if result {
60 | os.Exit(0)
61 | }
62 | log.Fatalf("Failed to sign in.")
63 | os.Exit(1)
64 | }
65 |
66 | result, err := h.IsSignedIn()
67 | if err != nil {
68 | panic(err)
69 | }
70 | if !result {
71 | log.Printf("Session is expired.")
72 | os.Exit(1)
73 | }
74 | if command == "check" {
75 | os.Exit(0)
76 | }
77 |
78 | switch command {
79 | case "status":
80 | {
81 | Command(h.GetStatus())
82 | }
83 | case "positions":
84 | {
85 | Command(h.GetPositions())
86 | }
87 | case "orders":
88 | {
89 | Command(h.GetOrders())
90 | }
91 | case "cancel":
92 | {
93 | Command(nil, h.CancelOrders())
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/utils/http.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "./cookiejar"
5 | "bytes"
6 | "github.com/PuerkitoBio/goquery"
7 | "golang.org/x/text/encoding/japanese"
8 | "golang.org/x/text/transform"
9 | "io"
10 | "io/ioutil"
11 | "net/http"
12 | "net/url"
13 | "os"
14 | "strings"
15 | )
16 |
17 | type HttpClient struct {
18 | initialized bool
19 | cookie *cookiejar.Jar
20 | UseSjis bool
21 | }
22 |
23 | func (h *HttpClient) getHttpClient() (*http.Client, error) {
24 | if !h.initialized {
25 | h.initialized = true
26 | if h.cookie == nil {
27 | cookie, err := cookiejar.New(nil)
28 | if err != nil {
29 | return nil, err
30 | }
31 | h.cookie = cookie
32 | }
33 | }
34 | return &http.Client{
35 | Jar: h.cookie,
36 | }, nil
37 | }
38 |
39 | func (h *HttpClient) sendRequest(method, urlStr string, body io.Reader) ([]byte, error) {
40 | req, err := http.NewRequest(method, urlStr, body)
41 | if err != nil {
42 | return nil, err
43 | }
44 | req.Header.Set(
45 | "User-Agent",
46 | "Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) "+
47 | "AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 "+
48 | "Mobile/12H321 Safari/600.1.4")
49 | if body != nil {
50 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
51 | }
52 | client, err := h.getHttpClient()
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | resp, err := client.Do(req)
58 | if err != nil {
59 | return nil, err
60 | }
61 | defer resp.Body.Close()
62 |
63 | return ioutil.ReadAll(resp.Body)
64 | }
65 |
66 | func (h *HttpClient) encode(buf []byte) ([]byte, error) {
67 | if !h.UseSjis {
68 | return buf, nil
69 | }
70 | return ioutil.ReadAll(transform.NewReader(bytes.NewReader(buf), japanese.ShiftJIS.NewEncoder()))
71 | }
72 |
73 | func (h *HttpClient) decode(buf []byte) ([]byte, error) {
74 | if !h.UseSjis {
75 | return buf, nil
76 | }
77 | return ioutil.ReadAll(transform.NewReader(bytes.NewReader(buf), japanese.ShiftJIS.NewDecoder()))
78 | }
79 |
80 | func (h *HttpClient) Save() error {
81 | return h.cookie.Save(os.Getenv("HOME") + "/.fxcookie")
82 | }
83 |
84 | func (h *HttpClient) Load() error {
85 | if h.cookie == nil {
86 | cookie, err := cookiejar.New(nil)
87 | if err != nil {
88 | return err
89 | }
90 | h.cookie = cookie
91 | }
92 | return h.cookie.Load(os.Getenv("HOME") + "/.fxcookie")
93 | }
94 |
95 | func (h *HttpClient) Do(method, urlStr string, query map[string]string) ([]byte, error) {
96 | values := url.Values{}
97 | for k, v := range query {
98 | buf, _ := h.encode([]byte(v))
99 | values.Add(k, string(buf))
100 | }
101 | q := values.Encode()
102 | var resp []byte
103 | var err error
104 | if len(query) == 0 {
105 | resp, err = h.sendRequest(method, urlStr, nil)
106 | } else if method == "GET" {
107 | resp, err = h.sendRequest(method, urlStr+"?"+q, nil)
108 | } else {
109 | resp, err = h.sendRequest(method, urlStr, strings.NewReader(q))
110 | }
111 | if err != nil {
112 | return nil, err
113 | }
114 | return h.decode(resp)
115 | }
116 |
117 | func (h *HttpClient) FetchDocument(method, urlStr string, query map[string]string) (*goquery.Document, error) {
118 | buf, err := h.Do(method, urlStr, query)
119 | if err != nil {
120 | return nil, err
121 | }
122 | return goquery.NewDocumentFromReader(bytes.NewReader(buf))
123 | }
124 |
--------------------------------------------------------------------------------
/hirose/status_test.go:
--------------------------------------------------------------------------------
1 | package hirose
2 |
3 | import (
4 | "github.com/PuerkitoBio/goquery"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestParseStatus(t *testing.T) {
10 | input := `
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | LION FX
19 |
20 |
21 |
22 |
23 |
>証拠金状況照会<
24 |
25 |
26 |
証拠金預託額:
27 |
28 | 1,850,000
29 |
30 |
ポジション損益:
31 |
32 | -257,710
33 |
34 |
未実現スワップ:
35 |
36 | 5,400
37 |
38 |
評価損益:
39 |
40 | -252,310
41 |
42 |
必要証拠金額:
43 |
44 | 960,000
45 |
46 |
発注証拠金額:
47 |
48 | 0
49 |
50 |
有効証拠金額:
51 |
52 | 1,597,690
53 |
54 |
有効比率:
55 |
166.42 %
56 |
アラート基準額:
57 |
58 | 1,920,000
59 |
60 |
ロスカット基準額:
61 |
62 | 960,000
63 |
64 |
発注可能額:
65 |
66 | 637,690
67 |
68 |
出金可能額:
69 |
70 | 637,690
71 |
72 |
出金依頼額:
73 |
74 | 0
75 |
76 |
金額指定全決済:
77 |
78 | 使わない
79 |
80 |
金額指定全決済判定基準:
81 |
82 | 評価損益
83 |
84 |
金額指定全決済(上限):
85 |
86 | ---
87 |
88 |
金額指定全決済(下限):
89 |
90 | ---
91 |
92 |
新規注文取消(金額指定):
93 |
94 | 取消しない
95 |
96 |
時間指定全決済:
97 |
98 | 使わない
99 |
100 |
全決済指定時間:
101 |
102 | ---
103 |
104 |
新規注文取消(時間指定):
105 |
106 | 取消しない
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | `
125 | doc, _ := goquery.NewDocumentFromReader(strings.NewReader(input))
126 | status, err := parseStatusPage(doc)
127 | if err != nil {
128 | t.Fatalf("got %v, want nil", err)
129 | }
130 | if *status.ActualDeposit != 1597690 {
131 | t.Errorf("ActualDeposit should be 1597690, but %d.", *status.ActualDeposit)
132 | }
133 | if *status.NecessaryDeposit != 960000 {
134 | t.Errorf("NecessaryDeposit should be 960000, but %d.", *status.NecessaryDeposit)
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/utils/cookiejar/punycode.go:
--------------------------------------------------------------------------------
1 | // Copyright 2012 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package cookiejar
6 |
7 | // This file implements the Punycode algorithm from RFC 3492.
8 |
9 | import (
10 | "fmt"
11 | "strings"
12 | "unicode/utf8"
13 | )
14 |
15 | // These parameter values are specified in section 5.
16 | //
17 | // All computation is done with int32s, so that overflow behavior is identical
18 | // regardless of whether int is 32-bit or 64-bit.
19 | const (
20 | base int32 = 36
21 | damp int32 = 700
22 | initialBias int32 = 72
23 | initialN int32 = 128
24 | skew int32 = 38
25 | tmax int32 = 26
26 | tmin int32 = 1
27 | )
28 |
29 | // encode encodes a string as specified in section 6.3 and prepends prefix to
30 | // the result.
31 | //
32 | // The "while h < length(input)" line in the specification becomes "for
33 | // remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes.
34 | func encode(prefix, s string) (string, error) {
35 | output := make([]byte, len(prefix), len(prefix)+1+2*len(s))
36 | copy(output, prefix)
37 | delta, n, bias := int32(0), initialN, initialBias
38 | b, remaining := int32(0), int32(0)
39 | for _, r := range s {
40 | if r < 0x80 {
41 | b++
42 | output = append(output, byte(r))
43 | } else {
44 | remaining++
45 | }
46 | }
47 | h := b
48 | if b > 0 {
49 | output = append(output, '-')
50 | }
51 | for remaining != 0 {
52 | m := int32(0x7fffffff)
53 | for _, r := range s {
54 | if m > r && r >= n {
55 | m = r
56 | }
57 | }
58 | delta += (m - n) * (h + 1)
59 | if delta < 0 {
60 | return "", fmt.Errorf("cookiejar: invalid label %q", s)
61 | }
62 | n = m
63 | for _, r := range s {
64 | if r < n {
65 | delta++
66 | if delta < 0 {
67 | return "", fmt.Errorf("cookiejar: invalid label %q", s)
68 | }
69 | continue
70 | }
71 | if r > n {
72 | continue
73 | }
74 | q := delta
75 | for k := base; ; k += base {
76 | t := k - bias
77 | if t < tmin {
78 | t = tmin
79 | } else if t > tmax {
80 | t = tmax
81 | }
82 | if q < t {
83 | break
84 | }
85 | output = append(output, encodeDigit(t+(q-t)%(base-t)))
86 | q = (q - t) / (base - t)
87 | }
88 | output = append(output, encodeDigit(q))
89 | bias = adapt(delta, h+1, h == b)
90 | delta = 0
91 | h++
92 | remaining--
93 | }
94 | delta++
95 | n++
96 | }
97 | return string(output), nil
98 | }
99 |
100 | func encodeDigit(digit int32) byte {
101 | switch {
102 | case 0 <= digit && digit < 26:
103 | return byte(digit + 'a')
104 | case 26 <= digit && digit < 36:
105 | return byte(digit + ('0' - 26))
106 | }
107 | panic("cookiejar: internal error in punycode encoding")
108 | }
109 |
110 | // adapt is the bias adaptation function specified in section 6.1.
111 | func adapt(delta, numPoints int32, firstTime bool) int32 {
112 | if firstTime {
113 | delta /= damp
114 | } else {
115 | delta /= 2
116 | }
117 | delta += delta / numPoints
118 | k := int32(0)
119 | for delta > ((base-tmin)*tmax)/2 {
120 | delta /= base - tmin
121 | k += base
122 | }
123 | return k + (base-tmin+1)*delta/(delta+skew)
124 | }
125 |
126 | // Strictly speaking, the remaining code below deals with IDNA (RFC 5890 and
127 | // friends) and not Punycode (RFC 3492) per se.
128 |
129 | // acePrefix is the ASCII Compatible Encoding prefix.
130 | const acePrefix = "xn--"
131 |
132 | // toASCII converts a domain or domain label to its ASCII form. For example,
133 | // toASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
134 | // toASCII("golang") is "golang".
135 | func toASCII(s string) (string, error) {
136 | if ascii(s) {
137 | return s, nil
138 | }
139 | labels := strings.Split(s, ".")
140 | for i, label := range labels {
141 | if !ascii(label) {
142 | a, err := encode(acePrefix, label)
143 | if err != nil {
144 | return "", err
145 | }
146 | labels[i] = a
147 | }
148 | }
149 | return strings.Join(labels, "."), nil
150 | }
151 |
152 | func ascii(s string) bool {
153 | for i := 0; i < len(s); i++ {
154 | if s[i] >= utf8.RuneSelf {
155 | return false
156 | }
157 | }
158 | return true
159 | }
160 |
--------------------------------------------------------------------------------
/hirose/position.go:
--------------------------------------------------------------------------------
1 | package hirose
2 |
3 | import (
4 | "fmt"
5 | "github.com/PuerkitoBio/goquery"
6 | "net/url"
7 | "strconv"
8 | "strings"
9 | "time"
10 | )
11 |
12 | type Position struct {
13 | PositionId string
14 | Currency string
15 | TransactionTime string
16 | Side string
17 | TransactionRate float64
18 | Amount int64
19 | }
20 |
21 | func getBaseQuery() map[string]string {
22 | t := time.Now()
23 | t.Add(time.Hour * 48)
24 | return map[string]string{
25 | "symbol_code": "",
26 | "f_y": "2015",
27 | "f_m": "01",
28 | "f_d": "01",
29 | "f_h": "00",
30 | "f_min": "00",
31 | "t_y": fmt.Sprintf("%d", t.Year()),
32 | "t_m": fmt.Sprintf("%d", t.Month()),
33 | "t_d": fmt.Sprintf("%d", t.Day()),
34 | "t_h": "00",
35 | "t_min": "00",
36 | }
37 | }
38 |
39 | func parsePositionListPage(s *goquery.Document) ([]Position, bool, error) {
40 | c := s.Find(".container").First()
41 | t := c.Find("div").First().Text()
42 | if t != ">ポジション情報(一覧)<" && t != ">ポジション情報(検索)<" {
43 | return nil, false, fmt.Errorf("cannot open \"ポジション情報(一覧)\", but %#v", t)
44 | }
45 |
46 | results := []Position{}
47 | c.Find("a").Each(
48 | func(_ int, s *goquery.Selection) {
49 | href, ok := s.Attr("href")
50 | if !ok || !strings.HasPrefix(href, "C304.html") {
51 | return
52 | }
53 | u, err := url.Parse(href)
54 | if err != nil || u.RawQuery == "" {
55 | return
56 | }
57 | v, err := url.ParseQuery(u.RawQuery)
58 | results = append(results, Position{
59 | PositionId: v.Get("position_id"),
60 | })
61 | })
62 |
63 | return results, c.Find("a[accesskey=\"#\"]").Length() == 1, nil
64 | }
65 |
66 | func (c *HiroseClient) GetPositionList() ([]Position, error) {
67 | positions := []Position{}
68 | for index := 1; index <= 100; index++ {
69 | query := getBaseQuery()
70 | query["page_index"] = fmt.Sprintf("%d", index)
71 | doc, err := c.FetchWithQuery("GET", "/otc/C302.html", query)
72 | if err != nil {
73 | return nil, err
74 | }
75 | p, nextPage, err := parsePositionListPage(doc)
76 | if err != nil {
77 | return nil, err
78 | }
79 | if !nextPage || len(p) == 0 {
80 | break
81 | }
82 | positions = append(positions, p...)
83 | }
84 | return positions, nil
85 | }
86 |
87 | func parsePositionDetailsPage(s *goquery.Document) (*Position, error) {
88 | c := s.Find(".container").First()
89 | t := c.Find("div").First().Text()
90 | if t != ">ポジション情報(詳細)<" {
91 | return nil, fmt.Errorf("cannot open \"ポジション情報(詳細)\", but %#v", t)
92 | }
93 |
94 | position := &Position{}
95 |
96 | // タイトル行の削除
97 | c.Find("hr").First().Next().PrevAll().Remove()
98 |
99 | // 通貨名の設定
100 | position.Currency = c.Find("div").First().Text()
101 | // 通貨名行の削除
102 | c.Find("hr").First().Next().PrevAll().Remove()
103 |
104 | // メニューの削除
105 | c.Find("hr").First().Prev().NextAll().Remove()
106 |
107 | results := []string{}
108 | c.Find("div").Each(func(_ int, s *goquery.Selection) {
109 | results = append(results, s.Text())
110 | })
111 |
112 | orig := map[string]string{}
113 | for i := 0; i+1 < len(results); i += 2 {
114 | k := strings.TrimSpace(strings.Replace(results[i], ":", "", -1))
115 | v := strings.TrimSpace(results[i+1])
116 | orig[k] = v
117 | }
118 |
119 | position.PositionId = orig["ポジション番号"]
120 | position.TransactionTime = orig["約定日時"]
121 | if orig["売買"] == "売" {
122 | position.Side = "sell"
123 | } else if orig["売買"] == "買" {
124 | position.Side = "buy"
125 | }
126 | rate, err := strconv.ParseFloat(orig["約定価格"], 64)
127 | if err != nil {
128 | return nil, fmt.Errorf("invalid transaction rate: %v", err)
129 | }
130 | position.TransactionRate = rate
131 | lot, err := strconv.ParseInt(orig["残Lot数"], 10, 64)
132 | position.Amount = lot * 1000
133 | return position, nil
134 | }
135 |
136 | func (c *HiroseClient) GetPosition(position Position) (*Position, error) {
137 | query := getBaseQuery()
138 | query["position_id"] = position.PositionId
139 | doc, err := c.FetchWithQuery("GET", "/otc/C304.html", query)
140 | if err != nil {
141 | return nil, err
142 | }
143 | return parsePositionDetailsPage(doc)
144 | }
145 |
146 | func (c *HiroseClient) GetPositions() ([]Position, error) {
147 | positions, err := c.GetPositionList()
148 | if err != nil {
149 | return nil, err
150 | }
151 |
152 | for i, position := range positions {
153 | p, err := c.GetPosition(position)
154 | if err != nil {
155 | return nil, err
156 | }
157 | positions[i] = *p
158 | }
159 |
160 | return positions, nil
161 | }
162 |
--------------------------------------------------------------------------------
/utils/cookiejar/punycode_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2012 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package cookiejar
6 |
7 | import (
8 | "testing"
9 | )
10 |
11 | var punycodeTestCases = [...]struct {
12 | s, encoded string
13 | }{
14 | {"", ""},
15 | {"-", "--"},
16 | {"-a", "-a-"},
17 | {"-a-", "-a--"},
18 | {"a", "a-"},
19 | {"a-", "a--"},
20 | {"a-b", "a-b-"},
21 | {"books", "books-"},
22 | {"bücher", "bcher-kva"},
23 | {"Hello世界", "Hello-ck1hg65u"},
24 | {"ü", "tda"},
25 | {"üý", "tdac"},
26 |
27 | // The test cases below come from RFC 3492 section 7.1 with Errata 3026.
28 | {
29 | // (A) Arabic (Egyptian).
30 | "\u0644\u064A\u0647\u0645\u0627\u0628\u062A\u0643\u0644" +
31 | "\u0645\u0648\u0634\u0639\u0631\u0628\u064A\u061F",
32 | "egbpdaj6bu4bxfgehfvwxn",
33 | },
34 | {
35 | // (B) Chinese (simplified).
36 | "\u4ED6\u4EEC\u4E3A\u4EC0\u4E48\u4E0D\u8BF4\u4E2D\u6587",
37 | "ihqwcrb4cv8a8dqg056pqjye",
38 | },
39 | {
40 | // (C) Chinese (traditional).
41 | "\u4ED6\u5011\u7232\u4EC0\u9EBD\u4E0D\u8AAA\u4E2D\u6587",
42 | "ihqwctvzc91f659drss3x8bo0yb",
43 | },
44 | {
45 | // (D) Czech.
46 | "\u0050\u0072\u006F\u010D\u0070\u0072\u006F\u0073\u0074" +
47 | "\u011B\u006E\u0065\u006D\u006C\u0075\u0076\u00ED\u010D" +
48 | "\u0065\u0073\u006B\u0079",
49 | "Proprostnemluvesky-uyb24dma41a",
50 | },
51 | {
52 | // (E) Hebrew.
53 | "\u05DC\u05DE\u05D4\u05D4\u05DD\u05E4\u05E9\u05D5\u05D8" +
54 | "\u05DC\u05D0\u05DE\u05D3\u05D1\u05E8\u05D9\u05DD\u05E2" +
55 | "\u05D1\u05E8\u05D9\u05EA",
56 | "4dbcagdahymbxekheh6e0a7fei0b",
57 | },
58 | {
59 | // (F) Hindi (Devanagari).
60 | "\u092F\u0939\u0932\u094B\u0917\u0939\u093F\u0928\u094D" +
61 | "\u0926\u0940\u0915\u094D\u092F\u094B\u0902\u0928\u0939" +
62 | "\u0940\u0902\u092C\u094B\u0932\u0938\u0915\u0924\u0947" +
63 | "\u0939\u0948\u0902",
64 | "i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd",
65 | },
66 | {
67 | // (G) Japanese (kanji and hiragana).
68 | "\u306A\u305C\u307F\u3093\u306A\u65E5\u672C\u8A9E\u3092" +
69 | "\u8A71\u3057\u3066\u304F\u308C\u306A\u3044\u306E\u304B",
70 | "n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa",
71 | },
72 | {
73 | // (H) Korean (Hangul syllables).
74 | "\uC138\uACC4\uC758\uBAA8\uB4E0\uC0AC\uB78C\uB4E4\uC774" +
75 | "\uD55C\uAD6D\uC5B4\uB97C\uC774\uD574\uD55C\uB2E4\uBA74" +
76 | "\uC5BC\uB9C8\uB098\uC88B\uC744\uAE4C",
77 | "989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5j" +
78 | "psd879ccm6fea98c",
79 | },
80 | {
81 | // (I) Russian (Cyrillic).
82 | "\u043F\u043E\u0447\u0435\u043C\u0443\u0436\u0435\u043E" +
83 | "\u043D\u0438\u043D\u0435\u0433\u043E\u0432\u043E\u0440" +
84 | "\u044F\u0442\u043F\u043E\u0440\u0443\u0441\u0441\u043A" +
85 | "\u0438",
86 | "b1abfaaepdrnnbgefbadotcwatmq2g4l",
87 | },
88 | {
89 | // (J) Spanish.
90 | "\u0050\u006F\u0072\u0071\u0075\u00E9\u006E\u006F\u0070" +
91 | "\u0075\u0065\u0064\u0065\u006E\u0073\u0069\u006D\u0070" +
92 | "\u006C\u0065\u006D\u0065\u006E\u0074\u0065\u0068\u0061" +
93 | "\u0062\u006C\u0061\u0072\u0065\u006E\u0045\u0073\u0070" +
94 | "\u0061\u00F1\u006F\u006C",
95 | "PorqunopuedensimplementehablarenEspaol-fmd56a",
96 | },
97 | {
98 | // (K) Vietnamese.
99 | "\u0054\u1EA1\u0069\u0073\u0061\u006F\u0068\u1ECD\u006B" +
100 | "\u0068\u00F4\u006E\u0067\u0074\u0068\u1EC3\u0063\u0068" +
101 | "\u1EC9\u006E\u00F3\u0069\u0074\u0069\u1EBF\u006E\u0067" +
102 | "\u0056\u0069\u1EC7\u0074",
103 | "TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g",
104 | },
105 | {
106 | // (L) 3B.
107 | "\u0033\u5E74\u0042\u7D44\u91D1\u516B\u5148\u751F",
108 | "3B-ww4c5e180e575a65lsy2b",
109 | },
110 | {
111 | // (M) -with-SUPER-MONKEYS.
112 | "\u5B89\u5BA4\u5948\u7F8E\u6075\u002D\u0077\u0069\u0074" +
113 | "\u0068\u002D\u0053\u0055\u0050\u0045\u0052\u002D\u004D" +
114 | "\u004F\u004E\u004B\u0045\u0059\u0053",
115 | "-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n",
116 | },
117 | {
118 | // (N) Hello-Another-Way-.
119 | "\u0048\u0065\u006C\u006C\u006F\u002D\u0041\u006E\u006F" +
120 | "\u0074\u0068\u0065\u0072\u002D\u0057\u0061\u0079\u002D" +
121 | "\u305D\u308C\u305E\u308C\u306E\u5834\u6240",
122 | "Hello-Another-Way--fc4qua05auwb3674vfr0b",
123 | },
124 | {
125 | // (O) 2.
126 | "\u3072\u3068\u3064\u5C4B\u6839\u306E\u4E0B\u0032",
127 | "2-u9tlzr9756bt3uc0v",
128 | },
129 | {
130 | // (P) MajiKoi5
131 | "\u004D\u0061\u006A\u0069\u3067\u004B\u006F\u0069\u3059" +
132 | "\u308B\u0035\u79D2\u524D",
133 | "MajiKoi5-783gue6qz075azm5e",
134 | },
135 | {
136 | // (Q) de
137 | "\u30D1\u30D5\u30A3\u30FC\u0064\u0065\u30EB\u30F3\u30D0",
138 | "de-jg4avhby1noc0d",
139 | },
140 | {
141 | // (R)
142 | "\u305D\u306E\u30B9\u30D4\u30FC\u30C9\u3067",
143 | "d9juau41awczczp",
144 | },
145 | {
146 | // (S) -> $1.00 <-
147 | "\u002D\u003E\u0020\u0024\u0031\u002E\u0030\u0030\u0020" +
148 | "\u003C\u002D",
149 | "-> $1.00 <--",
150 | },
151 | }
152 |
153 | func TestPunycode(t *testing.T) {
154 | for _, tc := range punycodeTestCases {
155 | if got, err := encode("", tc.s); err != nil {
156 | t.Errorf(`encode("", %q): %v`, tc.s, err)
157 | } else if got != tc.encoded {
158 | t.Errorf(`encode("", %q): got %q, want %q`, tc.s, got, tc.encoded)
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/hirose/order.go:
--------------------------------------------------------------------------------
1 | package hirose
2 |
3 | import (
4 | "fmt"
5 | "github.com/PuerkitoBio/goquery"
6 | "net/url"
7 | // "strconv"
8 | // "github.com/imos/go/var_dump"
9 | "errors"
10 | "strconv"
11 | "strings"
12 | "time"
13 | )
14 |
15 | type Order struct {
16 | OrderId string
17 | OrderMethod string
18 | Currency string
19 | PositionId string
20 | IsSettlement bool
21 | Side string
22 | IsStop bool
23 | Price float64
24 | Amount int64
25 | }
26 |
27 | func getBaseQueryForOrder() map[string]string {
28 | t := time.Now()
29 | t.Add(time.Hour * 48)
30 | return map[string]string{
31 | "page_index": "1", // NOTE: ヒロセ側が戻るページを生成するために必要
32 | "prev_page_flag": "1",
33 | "symbol_code": "",
34 | "open_close_type": "",
35 | "f_y": "2015",
36 | "f_m": "01",
37 | "f_d": "01",
38 | "f_h": "00",
39 | "f_min": "00",
40 | "t_y": fmt.Sprintf("%d", t.Year()),
41 | "t_m": fmt.Sprintf("%d", t.Month()),
42 | "t_d": fmt.Sprintf("%d", t.Day()),
43 | "t_h": "23",
44 | "t_min": "59",
45 | }
46 | }
47 |
48 | func parseOrderListPage(s *goquery.Document) ([]Order, bool, error) {
49 | c := s.Find(".container").First()
50 | t := c.Find("div").First().Text()
51 | if t != ">注文情報(一覧)<" && t != ">注文情報(検索)<" {
52 | return nil, false, fmt.Errorf("cannot open \"注文情報(一覧)\", but %#v", t)
53 | }
54 | // タイトル行の削除
55 | c.Find("hr").First().Next().PrevAll().Remove()
56 |
57 | results := []Order{}
58 | c.Find("a").Each(
59 | func(_ int, s *goquery.Selection) {
60 | href, ok := s.Attr("href")
61 | if !ok || !strings.HasPrefix(href, "../otc/C003.html?") {
62 | return
63 | }
64 | u, err := url.Parse(href)
65 | if err != nil || u.RawQuery == "" {
66 | return
67 | }
68 | v, err := url.ParseQuery(u.RawQuery)
69 | results = append(results, Order{
70 | OrderId: v.Get("order_id"),
71 | OrderMethod: v.Get("order_method"),
72 | })
73 | })
74 |
75 | return results, c.Find("a[accesskey=\"#\"]").Length() == 1, nil
76 | }
77 |
78 | func (c *HiroseClient) GetOrderList() ([]Order, error) {
79 | orders := []Order{}
80 | for index := 1; index <= 100; index++ {
81 | query := getBaseQueryForOrder()
82 | query["page_index"] = fmt.Sprintf("%d", index)
83 | doc, err := c.FetchWithQuery("GET", "/otc/C402.html", query)
84 | if err != nil {
85 | return nil, err
86 | }
87 | o, nextPage, err := parseOrderListPage(doc)
88 | if err != nil {
89 | return nil, err
90 | }
91 | if len(o) == 0 {
92 | break
93 | }
94 | orders = append(orders, o...)
95 | if !nextPage {
96 | break
97 | }
98 | }
99 | return orders, nil
100 | }
101 |
102 | func parseOrderDetailsPage(s *goquery.Document) (*Order, error) {
103 | c := s.Find(".container").First()
104 | t := c.Find("div").First().Text()
105 | if t != ">注文情報(詳細)<" {
106 | return nil, fmt.Errorf("cannot open \"注文情報(詳細)\", but %#v", t)
107 | }
108 |
109 | o := &Order{}
110 | // タイトル行の削除
111 | c.Find("hr").First().Next().PrevAll().Remove()
112 |
113 | c.Find("a").Each(func(_ int, s *goquery.Selection) {
114 | href, _ := s.Attr("href")
115 | // 取消リンク
116 | if strings.HasPrefix(href, "../otc/CT01.html?") {
117 | u, err := url.Parse(href)
118 | if err != nil || u.RawQuery == "" {
119 | return
120 | }
121 | v, err := url.ParseQuery(u.RawQuery)
122 | if err != nil {
123 | return
124 | }
125 | o.OrderId = v.Get("order_id")
126 | o.OrderMethod = v.Get("order_method")
127 | }
128 | // 指定決済リンク
129 | if strings.HasPrefix(href, "../common/I124.html?") {
130 | u, err := url.Parse(href)
131 | if err != nil || u.RawQuery == "" {
132 | return
133 | }
134 | v, err := url.ParseQuery(u.RawQuery)
135 | if err != nil {
136 | return
137 | }
138 | o.PositionId = v.Get("position_id")
139 | }
140 | })
141 |
142 | // メニューの削除
143 | c.Find("hr").First().Prev().NextAll().Remove()
144 |
145 | results := []string{}
146 | c.Find("div").Each(func(_ int, s *goquery.Selection) {
147 | results = append(results, s.Text())
148 | })
149 |
150 | orig := map[string]string{}
151 | for i := 0; i+1 < len(results); i += 2 {
152 | k := strings.TrimSpace(strings.Replace(results[i], ":", "", -1))
153 | v := strings.TrimSpace(results[i+1])
154 | orig[k] = v
155 | }
156 |
157 | o.Currency = orig["通貨ペア"]
158 | lot, err := strconv.ParseInt(orig["注文Lot数"], 10, 64)
159 | if err != nil {
160 | return nil, err
161 | }
162 | o.Amount = lot * 1000
163 | if orig["売買"] == "売" {
164 | o.Side = "sell"
165 | } else if orig["売買"] == "買" {
166 | o.Side = "buy"
167 | } else {
168 | return nil, fmt.Errorf("invalid value for 売買: %#v", orig["売買"])
169 | }
170 | if orig["執行条件"] == "指値" {
171 | o.IsStop = false
172 | } else if orig["執行条件"] == "逆指" {
173 | o.IsStop = true
174 | } else {
175 | return nil, fmt.Errorf("invalid value for 執行条件: %#v", orig["執行条件"])
176 | }
177 | o.Price, err = strconv.ParseFloat(orig["指定レート"], 64)
178 | if err != nil {
179 | return nil, err
180 | }
181 | if orig["注文区分"] == "指定決済" {
182 | o.IsSettlement = true
183 | } else if orig["注文区分"] == "売買" {
184 | o.IsSettlement = false
185 | } else {
186 | return nil, fmt.Errorf("invalid value for 注文区分: %#v", orig["注文区分"])
187 | }
188 |
189 | return o, nil
190 | }
191 |
192 | func (c *HiroseClient) GetOrder(order Order) (*Order, error) {
193 | query := getBaseQueryForOrder()
194 | query["order_id"] = order.OrderId
195 | query["order_method"] = order.OrderMethod
196 | doc, err := c.FetchWithQuery("GET", "/otc/C403.html", query)
197 | if err != nil {
198 | return nil, err
199 | }
200 | return parseOrderDetailsPage(doc)
201 | }
202 |
203 | func (c *HiroseClient) GetOrders() ([]Order, error) {
204 | orders, err := c.GetOrderList()
205 | if err != nil {
206 | return nil, err
207 | }
208 |
209 | for i, order := range orders {
210 | o, err := c.GetOrder(order)
211 | if err != nil {
212 | return nil, err
213 | }
214 | orders[i] = *o
215 | }
216 |
217 | return orders, nil
218 | }
219 |
220 | func (c *HiroseClient) CancelOrder(order Order) error {
221 | query := getBaseQueryForOrder()
222 | query["order_id"] = order.OrderId
223 | query["order_method"] = order.OrderMethod
224 | doc, err := c.FetchWithQuery("GET", "/otc/CT01.html", query)
225 | if err != nil {
226 | return err
227 | }
228 | f := doc.Find("form").First()
229 | if a, _ := f.Attr("action"); a != "CT02.html" {
230 | return errors.New("cancel button is not found.")
231 | }
232 |
233 | query = map[string]string{}
234 | f.Find("input").Each(func(_ int, s *goquery.Selection) {
235 | name, _ := s.Attr("name")
236 | value, _ := s.Attr("value")
237 | query[name] = value
238 | })
239 |
240 | doc, err = c.FetchWithQuery("POST", "/otc/CT02.html", query)
241 | if err != nil {
242 | return err
243 | }
244 | t := doc.Find("div").First().Text()
245 | if t != ">注文取消受付<" {
246 | return fmt.Errorf("cannot open \"注文取消受付\", but %#v", t)
247 | }
248 |
249 | return nil
250 | }
251 |
252 | func (c *HiroseClient) CancelOrders() error {
253 | orders, err := c.GetOrderList()
254 | if err != nil {
255 | return err
256 | }
257 |
258 | canceled := map[string]bool{}
259 | for _, order := range orders {
260 | if canceled[order.OrderId] {
261 | continue
262 | }
263 | err := c.CancelOrder(order)
264 | if err != nil {
265 | return fmt.Errorf("failed to cancel %s-%s: %#v", order.OrderId, order.OrderMethod, err)
266 | }
267 | canceled[order.OrderId] = true
268 | return nil
269 | }
270 |
271 | return nil
272 | }
273 |
--------------------------------------------------------------------------------
/hirose/position_test.go:
--------------------------------------------------------------------------------
1 | package hirose
2 |
3 | import (
4 | "github.com/PuerkitoBio/goquery"
5 | "reflect"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func TestParsePositionListPage(t *testing.T) {
11 | input := `
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | LION FX
20 |
21 |
22 |
23 |
24 |
>ポジション情報(一覧)<
25 |
26 |
27 |
28 |
29 |
30 | NZD/JPY
31 | 買
32 |
33 | 残Lot:10
34 |
35 |
36 | 約定値:78.303 評価:-7,050
37 |
38 |
決済
39 |
40 |
41 |
42 | NZD/JPY
43 | 買
44 |
45 | 残Lot:10
46 |
47 |
48 | 約定値:78.258 評価:-6,600
49 |
50 |
決済
51 |
52 |
53 |
54 | NZD/JPY
55 | 買
56 |
57 | 残Lot:10
58 |
59 |
60 | 約定値:78.339 評価:-7,410
61 |
62 |
決済
63 |
64 |
65 |
66 | NZD/JPY
67 | 買
68 |
69 | 残Lot:10
70 |
71 |
72 | 約定値:78.640 評価:-10,420
73 |
74 |
決済
75 |
76 |
77 |
78 | NZD/JPY
79 | 買
80 |
81 | 残Lot:10
82 |
83 |
84 | 約定値:78.558 評価:-9,600
85 |
86 |
決済
87 |
88 |
89 |
90 | NZD/JPY
91 | 買
92 |
93 | 残Lot:10
94 |
95 |
96 | 約定値:78.548 評価:-9,500
97 |
98 |
決済
99 |
100 |
101 |
102 | NZD/JPY
103 | 買
104 |
105 | 残Lot:10
106 |
107 |
108 | 約定値:78.536 評価:-9,380
109 |
110 |
決済
111 |
112 |
113 |
114 | NZD/JPY
115 | 買
116 |
117 | 残Lot:10
118 |
119 |
120 | 約定値:78.481 評価:-8,830
121 |
122 |
決済
123 |
124 |
125 |
126 | NZD/JPY
127 | 買
128 |
129 | 残Lot:100
130 |
131 |
132 | 約定値:78.534 評価:-93,600
133 |
134 |
決済
135 |
136 |
137 |
138 | NZD/JPY
139 | 買
140 |
141 | 残Lot:10
142 |
143 |
144 | 約定値:78.501 評価:-9,030
145 |
146 |
決済
147 |
148 |
149 |
150 |
151 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | `
171 | doc, _ := goquery.NewDocumentFromReader(strings.NewReader(input))
172 | positions, nextPage, err := parsePositionListPage(doc)
173 | if err != nil {
174 | t.Fatalf("got %v, want nil", err)
175 | }
176 | if !nextPage {
177 | t.Fatalf("nextPage got false, want true")
178 | }
179 | expected := []Position{
180 | Position{PositionId: "1603600030779000"},
181 | Position{PositionId: "1603600030714200"},
182 | Position{PositionId: "1603600030639100"},
183 | Position{PositionId: "1603600030449800"},
184 | Position{PositionId: "1603600030438900"},
185 | Position{PositionId: "1603600030435400"},
186 | Position{PositionId: "1603600030431300"},
187 | Position{PositionId: "1603600030414100"},
188 | Position{PositionId: "1603600030396800"},
189 | Position{PositionId: "1603600030382800"},
190 | }
191 | if !reflect.DeepEqual(expected, positions) {
192 | for k, p := range positions {
193 | t.Errorf("positions[%#v]=%#v", k, p)
194 | }
195 | t.Fatalf("wrong positions: %#v", positions)
196 | }
197 | }
198 |
199 | func TestParsePositionDetailsPage(t *testing.T) {
200 | input := `
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | LION FX
209 |
210 |
211 |
212 |
213 |
>ポジション情報(詳細)<
214 |
215 |
NZD/JPY
216 |
217 |
218 |
ポジション番号:
219 |
1603600030779000
220 |
約定日時:
221 |
2016/02/05 22:38:13
222 |
売買:
223 |
224 | 買
225 |
226 |
約定価格:
227 |
78.303
228 |
約定Lot数:
229 |
10
230 |
残Lot数:
231 |
10
232 |
ポジション損益:
233 |
234 | -7,110
235 |
236 |
未実現スワップ:
237 |
240 |
評価損益:
241 |
242 | -7,050
243 |
244 |
245 |
246 |
250 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 | `
274 | doc, _ := goquery.NewDocumentFromReader(strings.NewReader(input))
275 | position, err := parsePositionDetailsPage(doc)
276 | if err != nil {
277 | t.Fatalf("got %v, want nil", err)
278 | }
279 | expected := Position{
280 | Currency: "NZD/JPY",
281 | PositionId: "1603600030779000",
282 | TransactionTime: "2016/02/05 22:38:13",
283 | Side: "buy",
284 | TransactionRate: 78.303,
285 | Amount: 10000,
286 | }
287 | if !reflect.DeepEqual(&expected, position) {
288 | t.Fatalf("wrong positions: %#v", position)
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/utils/cookiejar/jar.go:
--------------------------------------------------------------------------------
1 | // Copyright 2012 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar.
6 | package cookiejar
7 |
8 | import (
9 | "errors"
10 | "fmt"
11 | "net"
12 | "net/http"
13 | "net/url"
14 | "sort"
15 | "strings"
16 | "sync"
17 | "time"
18 | )
19 |
20 | // PublicSuffixList provides the public suffix of a domain. For example:
21 | // - the public suffix of "example.com" is "com",
22 | // - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and
23 | // - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".
24 | //
25 | // Implementations of PublicSuffixList must be safe for concurrent use by
26 | // multiple goroutines.
27 | //
28 | // An implementation that always returns "" is valid and may be useful for
29 | // testing but it is not secure: it means that the HTTP server for foo.com can
30 | // set a cookie for bar.com.
31 | //
32 | // A public suffix list implementation is in the package
33 | // golang.org/x/net/publicsuffix.
34 | type PublicSuffixList interface {
35 | // PublicSuffix returns the public suffix of domain.
36 | //
37 | // TODO: specify which of the caller and callee is responsible for IP
38 | // addresses, for leading and trailing dots, for case sensitivity, and
39 | // for IDN/Punycode.
40 | PublicSuffix(domain string) string
41 |
42 | // String returns a description of the source of this public suffix
43 | // list. The description will typically contain something like a time
44 | // stamp or version number.
45 | String() string
46 | }
47 |
48 | // Options are the options for creating a new Jar.
49 | type Options struct {
50 | // PublicSuffixList is the public suffix list that determines whether
51 | // an HTTP server can set a cookie for a domain.
52 | //
53 | // A nil value is valid and may be useful for testing but it is not
54 | // secure: it means that the HTTP server for foo.co.uk can set a cookie
55 | // for bar.co.uk.
56 | PublicSuffixList PublicSuffixList
57 | }
58 |
59 | // Jar implements the http.CookieJar interface from the net/http package.
60 | type Jar struct {
61 | psList PublicSuffixList
62 |
63 | // mu locks the remaining fields.
64 | mu sync.Mutex
65 |
66 | // Entries is a set of Entries, keyed by their eTLD+1 and subkeyed by
67 | // their name/domain/path.
68 | Entries map[string]map[string]entry
69 |
70 | // NextSeqNum is the next sequence number assigned to a new cookie
71 | // created SetCookies.
72 | NextSeqNum uint64
73 | }
74 |
75 | // New returns a new cookie jar. A nil *Options is equivalent to a zero
76 | // Options.
77 | func New(o *Options) (*Jar, error) {
78 | jar := &Jar{
79 | Entries: make(map[string]map[string]entry),
80 | }
81 | if o != nil {
82 | jar.psList = o.PublicSuffixList
83 | }
84 | return jar, nil
85 | }
86 |
87 | // entry is the internal representation of a cookie.
88 | //
89 | // This struct type is not used outside of this package per se, but the exported
90 | // fields are those of RFC 6265.
91 | type entry struct {
92 | Name string
93 | Value string
94 | Domain string
95 | Path string
96 | Secure bool
97 | HttpOnly bool
98 | Persistent bool
99 | HostOnly bool
100 | Expires time.Time
101 | Creation time.Time
102 | LastAccess time.Time
103 |
104 | // SeqNum is a sequence number so that Cookies returns cookies in a
105 | // deterministic order, even for cookies that have equal Path length and
106 | // equal Creation time. This simplifies testing.
107 | SeqNum uint64
108 | }
109 |
110 | // Id returns the domain;path;name triple of e as an id.
111 | func (e *entry) id() string {
112 | return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)
113 | }
114 |
115 | // shouldSend determines whether e's cookie qualifies to be included in a
116 | // request to host/path. It is the caller's responsibility to check if the
117 | // cookie is expired.
118 | func (e *entry) shouldSend(https bool, host, path string) bool {
119 | return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
120 | }
121 |
122 | // domainMatch implements "domain-match" of RFC 6265 section 5.1.3.
123 | func (e *entry) domainMatch(host string) bool {
124 | if e.Domain == host {
125 | return true
126 | }
127 | return !e.HostOnly && hasDotSuffix(host, e.Domain)
128 | }
129 |
130 | // pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
131 | func (e *entry) pathMatch(requestPath string) bool {
132 | if requestPath == e.Path {
133 | return true
134 | }
135 | if strings.HasPrefix(requestPath, e.Path) {
136 | if e.Path[len(e.Path)-1] == '/' {
137 | return true // The "/any/" matches "/any/path" case.
138 | } else if requestPath[len(e.Path)] == '/' {
139 | return true // The "/any" matches "/any/path" case.
140 | }
141 | }
142 | return false
143 | }
144 |
145 | // hasDotSuffix reports whether s ends in "."+suffix.
146 | func hasDotSuffix(s, suffix string) bool {
147 | return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
148 | }
149 |
150 | // byPathLength is a []entry sort.Interface that sorts according to RFC 6265
151 | // section 5.4 point 2: by longest path and then by earliest creation time.
152 | type byPathLength []entry
153 |
154 | func (s byPathLength) Len() int { return len(s) }
155 |
156 | func (s byPathLength) Less(i, j int) bool {
157 | if len(s[i].Path) != len(s[j].Path) {
158 | return len(s[i].Path) > len(s[j].Path)
159 | }
160 | if !s[i].Creation.Equal(s[j].Creation) {
161 | return s[i].Creation.Before(s[j].Creation)
162 | }
163 | return s[i].SeqNum < s[j].SeqNum
164 | }
165 |
166 | func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
167 |
168 | // Cookies implements the Cookies method of the http.CookieJar interface.
169 | //
170 | // It returns an empty slice if the URL's scheme is not HTTP or HTTPS.
171 | func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
172 | return j.cookies(u, time.Now())
173 | }
174 |
175 | // cookies is like Cookies but takes the current time as a parameter.
176 | func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {
177 | if u.Scheme != "http" && u.Scheme != "https" {
178 | return cookies
179 | }
180 | host, err := canonicalHost(u.Host)
181 | if err != nil {
182 | return cookies
183 | }
184 | key := jarKey(host, j.psList)
185 |
186 | j.mu.Lock()
187 | defer j.mu.Unlock()
188 |
189 | submap := j.Entries[key]
190 | if submap == nil {
191 | return cookies
192 | }
193 |
194 | https := u.Scheme == "https"
195 | path := u.Path
196 | if path == "" {
197 | path = "/"
198 | }
199 |
200 | modified := false
201 | var selected []entry
202 | for id, e := range submap {
203 | if e.Persistent && !e.Expires.After(now) {
204 | delete(submap, id)
205 | modified = true
206 | continue
207 | }
208 | if !e.shouldSend(https, host, path) {
209 | continue
210 | }
211 | e.LastAccess = now
212 | submap[id] = e
213 | selected = append(selected, e)
214 | modified = true
215 | }
216 | if modified {
217 | if len(submap) == 0 {
218 | delete(j.Entries, key)
219 | } else {
220 | j.Entries[key] = submap
221 | }
222 | }
223 |
224 | sort.Sort(byPathLength(selected))
225 | for _, e := range selected {
226 | cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
227 | }
228 |
229 | return cookies
230 | }
231 |
232 | // SetCookies implements the SetCookies method of the http.CookieJar interface.
233 | //
234 | // It does nothing if the URL's scheme is not HTTP or HTTPS.
235 | func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
236 | j.setCookies(u, cookies, time.Now())
237 | }
238 |
239 | // setCookies is like SetCookies but takes the current time as parameter.
240 | func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
241 | if len(cookies) == 0 {
242 | return
243 | }
244 | if u.Scheme != "http" && u.Scheme != "https" {
245 | return
246 | }
247 | host, err := canonicalHost(u.Host)
248 | if err != nil {
249 | return
250 | }
251 | key := jarKey(host, j.psList)
252 | defPath := defaultPath(u.Path)
253 |
254 | j.mu.Lock()
255 | defer j.mu.Unlock()
256 |
257 | submap := j.Entries[key]
258 |
259 | modified := false
260 | for _, cookie := range cookies {
261 | e, remove, err := j.newEntry(cookie, now, defPath, host)
262 | if err != nil {
263 | continue
264 | }
265 | id := e.id()
266 | if remove {
267 | if submap != nil {
268 | if _, ok := submap[id]; ok {
269 | delete(submap, id)
270 | modified = true
271 | }
272 | }
273 | continue
274 | }
275 | if submap == nil {
276 | submap = make(map[string]entry)
277 | }
278 |
279 | if old, ok := submap[id]; ok {
280 | e.Creation = old.Creation
281 | e.SeqNum = old.SeqNum
282 | } else {
283 | e.Creation = now
284 | e.SeqNum = j.NextSeqNum
285 | j.NextSeqNum++
286 | }
287 | e.LastAccess = now
288 | submap[id] = e
289 | modified = true
290 | }
291 |
292 | if modified {
293 | if len(submap) == 0 {
294 | delete(j.Entries, key)
295 | } else {
296 | j.Entries[key] = submap
297 | }
298 | }
299 | }
300 |
301 | // canonicalHost strips port from host if present and returns the canonicalized
302 | // host name.
303 | func canonicalHost(host string) (string, error) {
304 | var err error
305 | host = strings.ToLower(host)
306 | if hasPort(host) {
307 | host, _, err = net.SplitHostPort(host)
308 | if err != nil {
309 | return "", err
310 | }
311 | }
312 | if strings.HasSuffix(host, ".") {
313 | // Strip trailing dot from fully qualified domain names.
314 | host = host[:len(host)-1]
315 | }
316 | return toASCII(host)
317 | }
318 |
319 | // hasPort reports whether host contains a port number. host may be a host
320 | // name, an IPv4 or an IPv6 address.
321 | func hasPort(host string) bool {
322 | colons := strings.Count(host, ":")
323 | if colons == 0 {
324 | return false
325 | }
326 | if colons == 1 {
327 | return true
328 | }
329 | return host[0] == '[' && strings.Contains(host, "]:")
330 | }
331 |
332 | // jarKey returns the key to use for a jar.
333 | func jarKey(host string, psl PublicSuffixList) string {
334 | if isIP(host) {
335 | return host
336 | }
337 |
338 | var i int
339 | if psl == nil {
340 | i = strings.LastIndex(host, ".")
341 | if i == -1 {
342 | return host
343 | }
344 | } else {
345 | suffix := psl.PublicSuffix(host)
346 | if suffix == host {
347 | return host
348 | }
349 | i = len(host) - len(suffix)
350 | if i <= 0 || host[i-1] != '.' {
351 | // The provided public suffix list psl is broken.
352 | // Storing cookies under host is a safe stopgap.
353 | return host
354 | }
355 | }
356 | prevDot := strings.LastIndex(host[:i-1], ".")
357 | return host[prevDot+1:]
358 | }
359 |
360 | // isIP reports whether host is an IP address.
361 | func isIP(host string) bool {
362 | return net.ParseIP(host) != nil
363 | }
364 |
365 | // defaultPath returns the directory part of an URL's path according to
366 | // RFC 6265 section 5.1.4.
367 | func defaultPath(path string) string {
368 | if len(path) == 0 || path[0] != '/' {
369 | return "/" // Path is empty or malformed.
370 | }
371 |
372 | i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.
373 | if i == 0 {
374 | return "/" // Path has the form "/abc".
375 | }
376 | return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".
377 | }
378 |
379 | // newEntry creates an entry from a http.Cookie c. now is the current time and
380 | // is compared to c.Expires to determine deletion of c. defPath and host are the
381 | // default-path and the canonical host name of the URL c was received from.
382 | //
383 | // remove records whether the jar should delete this cookie, as it has already
384 | // expired with respect to now. In this case, e may be incomplete, but it will
385 | // be valid to call e.id (which depends on e's Name, Domain and Path).
386 | //
387 | // A malformed c.Domain will result in an error.
388 | func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {
389 | e.Name = c.Name
390 |
391 | if c.Path == "" || c.Path[0] != '/' {
392 | e.Path = defPath
393 | } else {
394 | e.Path = c.Path
395 | }
396 |
397 | e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
398 | if err != nil {
399 | return e, false, err
400 | }
401 |
402 | // MaxAge takes precedence over Expires.
403 | if c.MaxAge < 0 {
404 | return e, true, nil
405 | } else if c.MaxAge > 0 {
406 | e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
407 | e.Persistent = true
408 | } else {
409 | if c.Expires.IsZero() {
410 | e.Expires = endOfTime
411 | e.Persistent = false
412 | } else {
413 | if !c.Expires.After(now) {
414 | return e, true, nil
415 | }
416 | e.Expires = c.Expires
417 | e.Persistent = true
418 | }
419 | }
420 |
421 | e.Value = c.Value
422 | e.Secure = c.Secure
423 | e.HttpOnly = c.HttpOnly
424 |
425 | return e, false, nil
426 | }
427 |
428 | var (
429 | errIllegalDomain = errors.New("cookiejar: illegal cookie domain attribute")
430 | errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")
431 | errNoHostname = errors.New("cookiejar: no host name available (IP only)")
432 | )
433 |
434 | // endOfTime is the time when session (non-persistent) cookies expire.
435 | // This instant is representable in most date/time formats (not just
436 | // Go's time.Time) and should be far enough in the future.
437 | var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
438 |
439 | // domainAndType determines the cookie's domain and hostOnly attribute.
440 | func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
441 | if domain == "" {
442 | // No domain attribute in the SetCookie header indicates a
443 | // host cookie.
444 | return host, true, nil
445 | }
446 |
447 | if isIP(host) {
448 | // According to RFC 6265 domain-matching includes not being
449 | // an IP address.
450 | // TODO: This might be relaxed as in common browsers.
451 | return "", false, errNoHostname
452 | }
453 |
454 | // From here on: If the cookie is valid, it is a domain cookie (with
455 | // the one exception of a public suffix below).
456 | // See RFC 6265 section 5.2.3.
457 | if domain[0] == '.' {
458 | domain = domain[1:]
459 | }
460 |
461 | if len(domain) == 0 || domain[0] == '.' {
462 | // Received either "Domain=." or "Domain=..some.thing",
463 | // both are illegal.
464 | return "", false, errMalformedDomain
465 | }
466 | domain = strings.ToLower(domain)
467 |
468 | if domain[len(domain)-1] == '.' {
469 | // We received stuff like "Domain=www.example.com.".
470 | // Browsers do handle such stuff (actually differently) but
471 | // RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
472 | // requiring a reject. 4.1.2.3 is not normative, but
473 | // "Domain Matching" (5.1.3) and "Canonicalized Host Names"
474 | // (5.1.2) are.
475 | return "", false, errMalformedDomain
476 | }
477 |
478 | // See RFC 6265 section 5.3 #5.
479 | if j.psList != nil {
480 | if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
481 | if host == domain {
482 | // This is the one exception in which a cookie
483 | // with a domain attribute is a host cookie.
484 | return host, true, nil
485 | }
486 | return "", false, errIllegalDomain
487 | }
488 | }
489 |
490 | // The domain must domain-match host: www.mycompany.com cannot
491 | // set cookies for .ourcompetitors.com.
492 | if host != domain && !hasDotSuffix(host, domain) {
493 | return "", false, errIllegalDomain
494 | }
495 |
496 | return domain, false, nil
497 | }
498 |
--------------------------------------------------------------------------------
/hirose/order_test.go:
--------------------------------------------------------------------------------
1 | package hirose
2 |
3 | import (
4 | "github.com/PuerkitoBio/goquery"
5 | "reflect"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func TestParseOrderListPage(t *testing.T) {
11 | input := `
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | LION FX
20 |
21 |
22 |
23 |
24 |
>注文情報(一覧)<
25 |
26 |
27 |
28 |
29 |
30 | NZD/JPY
31 | 売
32 |
33 | 区分:
34 | 指定決済
35 |
36 |
37 | 注Lot:10
38 | 執行:
39 | 指値
40 | (82.300)
41 |
42 |
43 | トリガー価格:---
44 |
45 |
変更
46 |
取消
47 |
48 |
49 |
50 | NZD/JPY
51 | 売
52 |
53 | 区分:
54 | 指定決済
55 |
56 |
57 | 注Lot:10
58 | 執行:
59 | 逆指
60 | (74.300)
61 |
62 |
63 | トリガー価格:---
64 |
65 |
変更
66 |
取消
67 |
68 |
69 |
70 | NZD/JPY
71 | 売
72 |
73 | 区分:
74 | 指定決済
75 |
76 |
77 | 注Lot:10
78 | 執行:
79 | 指値
80 | (82.300)
81 |
82 |
83 | トリガー価格:---
84 |
85 |
変更
86 |
取消
87 |
88 |
89 |
90 | NZD/JPY
91 | 売
92 |
93 | 区分:
94 | 指定決済
95 |
96 |
97 | 注Lot:10
98 | 執行:
99 | 逆指
100 | (74.300)
101 |
102 |
103 | トリガー価格:---
104 |
105 |
変更
106 |
取消
107 |
108 |
109 |
110 | NZD/JPY
111 | 売
112 |
113 | 区分:
114 | 指定決済
115 |
116 |
117 | 注Lot:10
118 | 執行:
119 | 指値
120 | (82.300)
121 |
122 |
123 | トリガー価格:---
124 |
125 |
変更
126 |
取消
127 |
128 |
129 |
130 | NZD/JPY
131 | 売
132 |
133 | 区分:
134 | 指定決済
135 |
136 |
137 | 注Lot:10
138 | 執行:
139 | 逆指
140 | (74.300)
141 |
142 |
143 | トリガー価格:---
144 |
145 |
変更
146 |
取消
147 |
148 |
149 |
150 | NZD/JPY
151 | 売
152 |
153 | 区分:
154 | 指定決済
155 |
156 |
157 | 注Lot:10
158 | 執行:
159 | 指値
160 | (82.300)
161 |
162 |
163 | トリガー価格:---
164 |
165 |
変更
166 |
取消
167 |
168 |
169 |
170 | NZD/JPY
171 | 売
172 |
173 | 区分:
174 | 指定決済
175 |
176 |
177 | 注Lot:10
178 | 執行:
179 | 逆指
180 | (74.300)
181 |
182 |
183 | トリガー価格:---
184 |
185 |
変更
186 |
取消
187 |
188 |
189 |
190 | NZD/JPY
191 | 売
192 |
193 | 区分:
194 | 指定決済
195 |
196 |
197 | 注Lot:100
198 | 執行:
199 | 指値
200 | (82.200)
201 |
202 |
203 | トリガー価格:---
204 |
205 |
変更
206 |
取消
207 |
208 |
209 |
210 | NZD/JPY
211 | 売
212 |
213 | 区分:
214 | 指定決済
215 |
216 |
217 | 注Lot:100
218 | 執行:
219 | 逆指
220 | (74.400)
221 |
222 |
223 | トリガー価格:---
224 |
225 |
変更
226 |
取消
227 |
228 |
229 |
230 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 | `
251 | doc, _ := goquery.NewDocumentFromReader(strings.NewReader(input))
252 | orders, nextPage, err := parseOrderListPage(doc)
253 | if err != nil {
254 | t.Fatalf("got %v, want nil", err)
255 | }
256 | if !nextPage {
257 | t.Fatalf("nextPage got false, want true")
258 | }
259 | expected := []Order{
260 | Order{OrderId: "1603600082705700", OrderMethod: "11"},
261 | Order{OrderId: "1603600082705700", OrderMethod: "12"},
262 | Order{OrderId: "1603600082672700", OrderMethod: "11"},
263 | Order{OrderId: "1603600082672700", OrderMethod: "12"},
264 | Order{OrderId: "1603600082617000", OrderMethod: "11"},
265 | Order{OrderId: "1603600082617000", OrderMethod: "12"},
266 | Order{OrderId: "1603600082542000", OrderMethod: "11"},
267 | Order{OrderId: "1603600082542000", OrderMethod: "12"},
268 | Order{OrderId: "1603600082436100", OrderMethod: "11"},
269 | Order{OrderId: "1603600082436100", OrderMethod: "12"},
270 | }
271 | if !reflect.DeepEqual(expected, orders) {
272 | for k, p := range orders {
273 | t.Errorf("orders[%#v]=%#v", k, p)
274 | }
275 | t.Fatalf("wrong orders: %#v", orders)
276 | }
277 | }
278 |
279 | func TestParseOrderDetailsPage_OcoSettlement(t *testing.T) {
280 | input := `
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 | LION FX
289 |
290 |
291 |
292 |
293 |
>注文情報(詳細)<
294 |
295 |
296 |
297 |
注文受付番号:
298 |
1603600082705700
299 |
通貨ペア:
300 |
NZD/JPY
301 |
注文状況:
302 |
303 | 注文中
304 |
305 |
注文手法:
306 |
309 |
両建:
310 |
311 | なし
312 |
313 |
決済順序:
314 |
315 | ---
316 |
317 |
318 |
決済オプション:
319 |
320 | ---
321 |
322 |
売買:
323 |
324 | 売
325 |
326 |
注文区分:
327 |
330 |
執行条件:
331 |
332 | 逆指
333 |
334 |
指定レート:
335 |
74.300
336 |
トレール:
337 |
---
338 |
注文Lot数:
339 |
10
340 |
期限:
341 |
342 |
343 | GTC
344 |
345 |
注文受付日時:
346 |
2016/02/05 22:57:41
347 |
348 |
349 |
350 |
351 |
変更
352 |
353 |
取消
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 | `
375 | doc, _ := goquery.NewDocumentFromReader(strings.NewReader(input))
376 | order, err := parseOrderDetailsPage(doc)
377 | if err != nil {
378 | t.Fatalf("got %v, want nil", err)
379 | }
380 | expected := Order{
381 | OrderId: "1603600082705700",
382 | OrderMethod: "12",
383 | Currency: "NZD/JPY",
384 | PositionId: "1603600030431300",
385 | IsSettlement: true,
386 | Side: "sell",
387 | IsStop: true,
388 | Price: 74.3,
389 | Amount: 10000,
390 | }
391 | if !reflect.DeepEqual(&expected, order) {
392 | t.Fatalf("wrong order: %#v", order)
393 | }
394 | }
395 |
--------------------------------------------------------------------------------
/utils/cookiejar/jar_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package cookiejar
6 |
7 | import (
8 | "fmt"
9 | "net/http"
10 | "net/url"
11 | "sort"
12 | "strings"
13 | "testing"
14 | "time"
15 | )
16 |
17 | // tNow is the synthetic current time used as now during testing.
18 | var tNow = time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC)
19 |
20 | // testPSL implements PublicSuffixList with just two rules: "co.uk"
21 | // and the default rule "*".
22 | type testPSL struct{}
23 |
24 | func (testPSL) String() string {
25 | return "testPSL"
26 | }
27 | func (testPSL) PublicSuffix(d string) string {
28 | if d == "co.uk" || strings.HasSuffix(d, ".co.uk") {
29 | return "co.uk"
30 | }
31 | return d[strings.LastIndex(d, ".")+1:]
32 | }
33 |
34 | // newTestJar creates an empty Jar with testPSL as the public suffix list.
35 | func newTestJar() *Jar {
36 | jar, err := New(&Options{PublicSuffixList: testPSL{}})
37 | if err != nil {
38 | panic(err)
39 | }
40 | return jar
41 | }
42 |
43 | var hasDotSuffixTests = [...]struct {
44 | s, suffix string
45 | }{
46 | {"", ""},
47 | {"", "."},
48 | {"", "x"},
49 | {".", ""},
50 | {".", "."},
51 | {".", ".."},
52 | {".", "x"},
53 | {".", "x."},
54 | {".", ".x"},
55 | {".", ".x."},
56 | {"x", ""},
57 | {"x", "."},
58 | {"x", ".."},
59 | {"x", "x"},
60 | {"x", "x."},
61 | {"x", ".x"},
62 | {"x", ".x."},
63 | {".x", ""},
64 | {".x", "."},
65 | {".x", ".."},
66 | {".x", "x"},
67 | {".x", "x."},
68 | {".x", ".x"},
69 | {".x", ".x."},
70 | {"x.", ""},
71 | {"x.", "."},
72 | {"x.", ".."},
73 | {"x.", "x"},
74 | {"x.", "x."},
75 | {"x.", ".x"},
76 | {"x.", ".x."},
77 | {"com", ""},
78 | {"com", "m"},
79 | {"com", "om"},
80 | {"com", "com"},
81 | {"com", ".com"},
82 | {"com", "x.com"},
83 | {"com", "xcom"},
84 | {"com", "xorg"},
85 | {"com", "org"},
86 | {"com", "rg"},
87 | {"foo.com", ""},
88 | {"foo.com", "m"},
89 | {"foo.com", "om"},
90 | {"foo.com", "com"},
91 | {"foo.com", ".com"},
92 | {"foo.com", "o.com"},
93 | {"foo.com", "oo.com"},
94 | {"foo.com", "foo.com"},
95 | {"foo.com", ".foo.com"},
96 | {"foo.com", "x.foo.com"},
97 | {"foo.com", "xfoo.com"},
98 | {"foo.com", "xfoo.org"},
99 | {"foo.com", "foo.org"},
100 | {"foo.com", "oo.org"},
101 | {"foo.com", "o.org"},
102 | {"foo.com", ".org"},
103 | {"foo.com", "org"},
104 | {"foo.com", "rg"},
105 | }
106 |
107 | func TestHasDotSuffix(t *testing.T) {
108 | for _, tc := range hasDotSuffixTests {
109 | got := hasDotSuffix(tc.s, tc.suffix)
110 | want := strings.HasSuffix(tc.s, "."+tc.suffix)
111 | if got != want {
112 | t.Errorf("s=%q, suffix=%q: got %v, want %v", tc.s, tc.suffix, got, want)
113 | }
114 | }
115 | }
116 |
117 | var canonicalHostTests = map[string]string{
118 | "www.example.com": "www.example.com",
119 | "WWW.EXAMPLE.COM": "www.example.com",
120 | "wWw.eXAmple.CoM": "www.example.com",
121 | "www.example.com:80": "www.example.com",
122 | "192.168.0.10": "192.168.0.10",
123 | "192.168.0.5:8080": "192.168.0.5",
124 | "2001:4860:0:2001::68": "2001:4860:0:2001::68",
125 | "[2001:4860:0:::68]:8080": "2001:4860:0:::68",
126 | "www.bücher.de": "www.xn--bcher-kva.de",
127 | "www.example.com.": "www.example.com",
128 | "[bad.unmatched.bracket:": "error",
129 | }
130 |
131 | func TestCanonicalHost(t *testing.T) {
132 | for h, want := range canonicalHostTests {
133 | got, err := canonicalHost(h)
134 | if want == "error" {
135 | if err == nil {
136 | t.Errorf("%q: got nil error, want non-nil", h)
137 | }
138 | continue
139 | }
140 | if err != nil {
141 | t.Errorf("%q: %v", h, err)
142 | continue
143 | }
144 | if got != want {
145 | t.Errorf("%q: got %q, want %q", h, got, want)
146 | continue
147 | }
148 | }
149 | }
150 |
151 | var hasPortTests = map[string]bool{
152 | "www.example.com": false,
153 | "www.example.com:80": true,
154 | "127.0.0.1": false,
155 | "127.0.0.1:8080": true,
156 | "2001:4860:0:2001::68": false,
157 | "[2001::0:::68]:80": true,
158 | }
159 |
160 | func TestHasPort(t *testing.T) {
161 | for host, want := range hasPortTests {
162 | if got := hasPort(host); got != want {
163 | t.Errorf("%q: got %t, want %t", host, got, want)
164 | }
165 | }
166 | }
167 |
168 | var jarKeyTests = map[string]string{
169 | "foo.www.example.com": "example.com",
170 | "www.example.com": "example.com",
171 | "example.com": "example.com",
172 | "com": "com",
173 | "foo.www.bbc.co.uk": "bbc.co.uk",
174 | "www.bbc.co.uk": "bbc.co.uk",
175 | "bbc.co.uk": "bbc.co.uk",
176 | "co.uk": "co.uk",
177 | "uk": "uk",
178 | "192.168.0.5": "192.168.0.5",
179 | }
180 |
181 | func TestJarKey(t *testing.T) {
182 | for host, want := range jarKeyTests {
183 | if got := jarKey(host, testPSL{}); got != want {
184 | t.Errorf("%q: got %q, want %q", host, got, want)
185 | }
186 | }
187 | }
188 |
189 | var jarKeyNilPSLTests = map[string]string{
190 | "foo.www.example.com": "example.com",
191 | "www.example.com": "example.com",
192 | "example.com": "example.com",
193 | "com": "com",
194 | "foo.www.bbc.co.uk": "co.uk",
195 | "www.bbc.co.uk": "co.uk",
196 | "bbc.co.uk": "co.uk",
197 | "co.uk": "co.uk",
198 | "uk": "uk",
199 | "192.168.0.5": "192.168.0.5",
200 | }
201 |
202 | func TestJarKeyNilPSL(t *testing.T) {
203 | for host, want := range jarKeyNilPSLTests {
204 | if got := jarKey(host, nil); got != want {
205 | t.Errorf("%q: got %q, want %q", host, got, want)
206 | }
207 | }
208 | }
209 |
210 | var isIPTests = map[string]bool{
211 | "127.0.0.1": true,
212 | "1.2.3.4": true,
213 | "2001:4860:0:2001::68": true,
214 | "example.com": false,
215 | "1.1.1.300": false,
216 | "www.foo.bar.net": false,
217 | "123.foo.bar.net": false,
218 | }
219 |
220 | func TestIsIP(t *testing.T) {
221 | for host, want := range isIPTests {
222 | if got := isIP(host); got != want {
223 | t.Errorf("%q: got %t, want %t", host, got, want)
224 | }
225 | }
226 | }
227 |
228 | var defaultPathTests = map[string]string{
229 | "/": "/",
230 | "/abc": "/",
231 | "/abc/": "/abc",
232 | "/abc/xyz": "/abc",
233 | "/abc/xyz/": "/abc/xyz",
234 | "/a/b/c.html": "/a/b",
235 | "": "/",
236 | "strange": "/",
237 | "//": "/",
238 | "/a//b": "/a/",
239 | "/a/./b": "/a/.",
240 | "/a/../b": "/a/..",
241 | }
242 |
243 | func TestDefaultPath(t *testing.T) {
244 | for path, want := range defaultPathTests {
245 | if got := defaultPath(path); got != want {
246 | t.Errorf("%q: got %q, want %q", path, got, want)
247 | }
248 | }
249 | }
250 |
251 | var domainAndTypeTests = [...]struct {
252 | host string // host Set-Cookie header was received from
253 | domain string // domain attribute in Set-Cookie header
254 | wantDomain string // expected domain of cookie
255 | wantHostOnly bool // expected host-cookie flag
256 | wantErr error // expected error
257 | }{
258 | {"www.example.com", "", "www.example.com", true, nil},
259 | {"127.0.0.1", "", "127.0.0.1", true, nil},
260 | {"2001:4860:0:2001::68", "", "2001:4860:0:2001::68", true, nil},
261 | {"www.example.com", "example.com", "example.com", false, nil},
262 | {"www.example.com", ".example.com", "example.com", false, nil},
263 | {"www.example.com", "www.example.com", "www.example.com", false, nil},
264 | {"www.example.com", ".www.example.com", "www.example.com", false, nil},
265 | {"foo.sso.example.com", "sso.example.com", "sso.example.com", false, nil},
266 | {"bar.co.uk", "bar.co.uk", "bar.co.uk", false, nil},
267 | {"foo.bar.co.uk", ".bar.co.uk", "bar.co.uk", false, nil},
268 | {"127.0.0.1", "127.0.0.1", "", false, errNoHostname},
269 | {"2001:4860:0:2001::68", "2001:4860:0:2001::68", "2001:4860:0:2001::68", false, errNoHostname},
270 | {"www.example.com", ".", "", false, errMalformedDomain},
271 | {"www.example.com", "..", "", false, errMalformedDomain},
272 | {"www.example.com", "other.com", "", false, errIllegalDomain},
273 | {"www.example.com", "com", "", false, errIllegalDomain},
274 | {"www.example.com", ".com", "", false, errIllegalDomain},
275 | {"foo.bar.co.uk", ".co.uk", "", false, errIllegalDomain},
276 | {"127.www.0.0.1", "127.0.0.1", "", false, errIllegalDomain},
277 | {"com", "", "com", true, nil},
278 | {"com", "com", "com", true, nil},
279 | {"com", ".com", "com", true, nil},
280 | {"co.uk", "", "co.uk", true, nil},
281 | {"co.uk", "co.uk", "co.uk", true, nil},
282 | {"co.uk", ".co.uk", "co.uk", true, nil},
283 | }
284 |
285 | func TestDomainAndType(t *testing.T) {
286 | jar := newTestJar()
287 | for _, tc := range domainAndTypeTests {
288 | domain, hostOnly, err := jar.domainAndType(tc.host, tc.domain)
289 | if err != tc.wantErr {
290 | t.Errorf("%q/%q: got %q error, want %q",
291 | tc.host, tc.domain, err, tc.wantErr)
292 | continue
293 | }
294 | if err != nil {
295 | continue
296 | }
297 | if domain != tc.wantDomain || hostOnly != tc.wantHostOnly {
298 | t.Errorf("%q/%q: got %q/%t want %q/%t",
299 | tc.host, tc.domain, domain, hostOnly,
300 | tc.wantDomain, tc.wantHostOnly)
301 | }
302 | }
303 | }
304 |
305 | // expiresIn creates an expires attribute delta seconds from tNow.
306 | func expiresIn(delta int) string {
307 | t := tNow.Add(time.Duration(delta) * time.Second)
308 | return "expires=" + t.Format(time.RFC1123)
309 | }
310 |
311 | // mustParseURL parses s to an URL and panics on error.
312 | func mustParseURL(s string) *url.URL {
313 | u, err := url.Parse(s)
314 | if err != nil || u.Scheme == "" || u.Host == "" {
315 | panic(fmt.Sprintf("Unable to parse URL %s.", s))
316 | }
317 | return u
318 | }
319 |
320 | // jarTest encapsulates the following actions on a jar:
321 | // 1. Perform SetCookies with fromURL and the cookies from setCookies.
322 | // (Done at time tNow + 0 ms.)
323 | // 2. Check that the Entries in the jar matches content.
324 | // (Done at time tNow + 1001 ms.)
325 | // 3. For each query in tests: Check that Cookies with toURL yields the
326 | // cookies in want.
327 | // (Query n done at tNow + (n+2)*1001 ms.)
328 | type jarTest struct {
329 | description string // The description of what this test is supposed to test
330 | fromURL string // The full URL of the request from which Set-Cookie headers where received
331 | setCookies []string // All the cookies received from fromURL
332 | content string // The whole (non-expired) content of the jar
333 | queries []query // Queries to test the Jar.Cookies method
334 | }
335 |
336 | // query contains one test of the cookies returned from Jar.Cookies.
337 | type query struct {
338 | toURL string // the URL in the Cookies call
339 | want string // the expected list of cookies (order matters)
340 | }
341 |
342 | // run runs the jarTest.
343 | func (test jarTest) run(t *testing.T, jar *Jar) {
344 | now := tNow
345 |
346 | // Populate jar with cookies.
347 | setCookies := make([]*http.Cookie, len(test.setCookies))
348 | for i, cs := range test.setCookies {
349 | cookies := (&http.Response{Header: http.Header{"Set-Cookie": {cs}}}).Cookies()
350 | if len(cookies) != 1 {
351 | panic(fmt.Sprintf("Wrong cookie line %q: %#v", cs, cookies))
352 | }
353 | setCookies[i] = cookies[0]
354 | }
355 | jar.setCookies(mustParseURL(test.fromURL), setCookies, now)
356 | now = now.Add(1001 * time.Millisecond)
357 |
358 | // Serialize non-expired Entries in the form "name1=val1 name2=val2".
359 | var cs []string
360 | for _, submap := range jar.Entries {
361 | for _, cookie := range submap {
362 | if !cookie.Expires.After(now) {
363 | continue
364 | }
365 | cs = append(cs, cookie.Name+"="+cookie.Value)
366 | }
367 | }
368 | sort.Strings(cs)
369 | got := strings.Join(cs, " ")
370 |
371 | // Make sure jar content matches our expectations.
372 | if got != test.content {
373 | t.Errorf("Test %q Content\ngot %q\nwant %q",
374 | test.description, got, test.content)
375 | }
376 |
377 | // Test different calls to Cookies.
378 | for i, query := range test.queries {
379 | now = now.Add(1001 * time.Millisecond)
380 | var s []string
381 | for _, c := range jar.cookies(mustParseURL(query.toURL), now) {
382 | s = append(s, c.Name+"="+c.Value)
383 | }
384 | if got := strings.Join(s, " "); got != query.want {
385 | t.Errorf("Test %q #%d\ngot %q\nwant %q", test.description, i, got, query.want)
386 | }
387 | }
388 | }
389 |
390 | // basicsTests contains fundamental tests. Each jarTest has to be performed on
391 | // a fresh, empty Jar.
392 | var basicsTests = [...]jarTest{
393 | {
394 | "Retrieval of a plain host cookie.",
395 | "http://www.host.test/",
396 | []string{"A=a"},
397 | "A=a",
398 | []query{
399 | {"http://www.host.test", "A=a"},
400 | {"http://www.host.test/", "A=a"},
401 | {"http://www.host.test/some/path", "A=a"},
402 | {"https://www.host.test", "A=a"},
403 | {"https://www.host.test/", "A=a"},
404 | {"https://www.host.test/some/path", "A=a"},
405 | {"ftp://www.host.test", ""},
406 | {"ftp://www.host.test/", ""},
407 | {"ftp://www.host.test/some/path", ""},
408 | {"http://www.other.org", ""},
409 | {"http://sibling.host.test", ""},
410 | {"http://deep.www.host.test", ""},
411 | },
412 | },
413 | {
414 | "Secure cookies are not returned to http.",
415 | "http://www.host.test/",
416 | []string{"A=a; secure"},
417 | "A=a",
418 | []query{
419 | {"http://www.host.test", ""},
420 | {"http://www.host.test/", ""},
421 | {"http://www.host.test/some/path", ""},
422 | {"https://www.host.test", "A=a"},
423 | {"https://www.host.test/", "A=a"},
424 | {"https://www.host.test/some/path", "A=a"},
425 | },
426 | },
427 | {
428 | "Explicit path.",
429 | "http://www.host.test/",
430 | []string{"A=a; path=/some/path"},
431 | "A=a",
432 | []query{
433 | {"http://www.host.test", ""},
434 | {"http://www.host.test/", ""},
435 | {"http://www.host.test/some", ""},
436 | {"http://www.host.test/some/", ""},
437 | {"http://www.host.test/some/path", "A=a"},
438 | {"http://www.host.test/some/paths", ""},
439 | {"http://www.host.test/some/path/foo", "A=a"},
440 | {"http://www.host.test/some/path/foo/", "A=a"},
441 | },
442 | },
443 | {
444 | "Implicit path #1: path is a directory.",
445 | "http://www.host.test/some/path/",
446 | []string{"A=a"},
447 | "A=a",
448 | []query{
449 | {"http://www.host.test", ""},
450 | {"http://www.host.test/", ""},
451 | {"http://www.host.test/some", ""},
452 | {"http://www.host.test/some/", ""},
453 | {"http://www.host.test/some/path", "A=a"},
454 | {"http://www.host.test/some/paths", ""},
455 | {"http://www.host.test/some/path/foo", "A=a"},
456 | {"http://www.host.test/some/path/foo/", "A=a"},
457 | },
458 | },
459 | {
460 | "Implicit path #2: path is not a directory.",
461 | "http://www.host.test/some/path/index.html",
462 | []string{"A=a"},
463 | "A=a",
464 | []query{
465 | {"http://www.host.test", ""},
466 | {"http://www.host.test/", ""},
467 | {"http://www.host.test/some", ""},
468 | {"http://www.host.test/some/", ""},
469 | {"http://www.host.test/some/path", "A=a"},
470 | {"http://www.host.test/some/paths", ""},
471 | {"http://www.host.test/some/path/foo", "A=a"},
472 | {"http://www.host.test/some/path/foo/", "A=a"},
473 | },
474 | },
475 | {
476 | "Implicit path #3: no path in URL at all.",
477 | "http://www.host.test",
478 | []string{"A=a"},
479 | "A=a",
480 | []query{
481 | {"http://www.host.test", "A=a"},
482 | {"http://www.host.test/", "A=a"},
483 | {"http://www.host.test/some/path", "A=a"},
484 | },
485 | },
486 | {
487 | "Cookies are sorted by path length.",
488 | "http://www.host.test/",
489 | []string{
490 | "A=a; path=/foo/bar",
491 | "B=b; path=/foo/bar/baz/qux",
492 | "C=c; path=/foo/bar/baz",
493 | "D=d; path=/foo"},
494 | "A=a B=b C=c D=d",
495 | []query{
496 | {"http://www.host.test/foo/bar/baz/qux", "B=b C=c A=a D=d"},
497 | {"http://www.host.test/foo/bar/baz/", "C=c A=a D=d"},
498 | {"http://www.host.test/foo/bar", "A=a D=d"},
499 | },
500 | },
501 | {
502 | "Creation time determines sorting on same length paths.",
503 | "http://www.host.test/",
504 | []string{
505 | "A=a; path=/foo/bar",
506 | "X=x; path=/foo/bar",
507 | "Y=y; path=/foo/bar/baz/qux",
508 | "B=b; path=/foo/bar/baz/qux",
509 | "C=c; path=/foo/bar/baz",
510 | "W=w; path=/foo/bar/baz",
511 | "Z=z; path=/foo",
512 | "D=d; path=/foo"},
513 | "A=a B=b C=c D=d W=w X=x Y=y Z=z",
514 | []query{
515 | {"http://www.host.test/foo/bar/baz/qux", "Y=y B=b C=c W=w A=a X=x Z=z D=d"},
516 | {"http://www.host.test/foo/bar/baz/", "C=c W=w A=a X=x Z=z D=d"},
517 | {"http://www.host.test/foo/bar", "A=a X=x Z=z D=d"},
518 | },
519 | },
520 | {
521 | "Sorting of same-name cookies.",
522 | "http://www.host.test/",
523 | []string{
524 | "A=1; path=/",
525 | "A=2; path=/path",
526 | "A=3; path=/quux",
527 | "A=4; path=/path/foo",
528 | "A=5; domain=.host.test; path=/path",
529 | "A=6; domain=.host.test; path=/quux",
530 | "A=7; domain=.host.test; path=/path/foo",
531 | },
532 | "A=1 A=2 A=3 A=4 A=5 A=6 A=7",
533 | []query{
534 | {"http://www.host.test/path", "A=2 A=5 A=1"},
535 | {"http://www.host.test/path/foo", "A=4 A=7 A=2 A=5 A=1"},
536 | },
537 | },
538 | {
539 | "Disallow domain cookie on public suffix.",
540 | "http://www.bbc.co.uk",
541 | []string{
542 | "a=1",
543 | "b=2; domain=co.uk",
544 | },
545 | "a=1",
546 | []query{{"http://www.bbc.co.uk", "a=1"}},
547 | },
548 | {
549 | "Host cookie on IP.",
550 | "http://192.168.0.10",
551 | []string{"a=1"},
552 | "a=1",
553 | []query{{"http://192.168.0.10", "a=1"}},
554 | },
555 | {
556 | "Port is ignored #1.",
557 | "http://www.host.test/",
558 | []string{"a=1"},
559 | "a=1",
560 | []query{
561 | {"http://www.host.test", "a=1"},
562 | {"http://www.host.test:8080/", "a=1"},
563 | },
564 | },
565 | {
566 | "Port is ignored #2.",
567 | "http://www.host.test:8080/",
568 | []string{"a=1"},
569 | "a=1",
570 | []query{
571 | {"http://www.host.test", "a=1"},
572 | {"http://www.host.test:8080/", "a=1"},
573 | {"http://www.host.test:1234/", "a=1"},
574 | },
575 | },
576 | }
577 |
578 | func TestBasics(t *testing.T) {
579 | for _, test := range basicsTests {
580 | jar := newTestJar()
581 | test.run(t, jar)
582 | }
583 | }
584 |
585 | // updateAndDeleteTests contains jarTests which must be performed on the same
586 | // Jar.
587 | var updateAndDeleteTests = [...]jarTest{
588 | {
589 | "Set initial cookies.",
590 | "http://www.host.test",
591 | []string{
592 | "a=1",
593 | "b=2; secure",
594 | "c=3; httponly",
595 | "d=4; secure; httponly"},
596 | "a=1 b=2 c=3 d=4",
597 | []query{
598 | {"http://www.host.test", "a=1 c=3"},
599 | {"https://www.host.test", "a=1 b=2 c=3 d=4"},
600 | },
601 | },
602 | {
603 | "Update value via http.",
604 | "http://www.host.test",
605 | []string{
606 | "a=w",
607 | "b=x; secure",
608 | "c=y; httponly",
609 | "d=z; secure; httponly"},
610 | "a=w b=x c=y d=z",
611 | []query{
612 | {"http://www.host.test", "a=w c=y"},
613 | {"https://www.host.test", "a=w b=x c=y d=z"},
614 | },
615 | },
616 | {
617 | "Clear Secure flag from a http.",
618 | "http://www.host.test/",
619 | []string{
620 | "b=xx",
621 | "d=zz; httponly"},
622 | "a=w b=xx c=y d=zz",
623 | []query{{"http://www.host.test", "a=w b=xx c=y d=zz"}},
624 | },
625 | {
626 | "Delete all.",
627 | "http://www.host.test/",
628 | []string{
629 | "a=1; max-Age=-1", // delete via MaxAge
630 | "b=2; " + expiresIn(-10), // delete via Expires
631 | "c=2; max-age=-1; " + expiresIn(-10), // delete via both
632 | "d=4; max-age=-1; " + expiresIn(10)}, // MaxAge takes precedence
633 | "",
634 | []query{{"http://www.host.test", ""}},
635 | },
636 | {
637 | "Refill #1.",
638 | "http://www.host.test",
639 | []string{
640 | "A=1",
641 | "A=2; path=/foo",
642 | "A=3; domain=.host.test",
643 | "A=4; path=/foo; domain=.host.test"},
644 | "A=1 A=2 A=3 A=4",
645 | []query{{"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}},
646 | },
647 | {
648 | "Refill #2.",
649 | "http://www.google.com",
650 | []string{
651 | "A=6",
652 | "A=7; path=/foo",
653 | "A=8; domain=.google.com",
654 | "A=9; path=/foo; domain=.google.com"},
655 | "A=1 A=2 A=3 A=4 A=6 A=7 A=8 A=9",
656 | []query{
657 | {"http://www.host.test/foo", "A=2 A=4 A=1 A=3"},
658 | {"http://www.google.com/foo", "A=7 A=9 A=6 A=8"},
659 | },
660 | },
661 | {
662 | "Delete A7.",
663 | "http://www.google.com",
664 | []string{"A=; path=/foo; max-age=-1"},
665 | "A=1 A=2 A=3 A=4 A=6 A=8 A=9",
666 | []query{
667 | {"http://www.host.test/foo", "A=2 A=4 A=1 A=3"},
668 | {"http://www.google.com/foo", "A=9 A=6 A=8"},
669 | },
670 | },
671 | {
672 | "Delete A4.",
673 | "http://www.host.test",
674 | []string{"A=; path=/foo; domain=host.test; max-age=-1"},
675 | "A=1 A=2 A=3 A=6 A=8 A=9",
676 | []query{
677 | {"http://www.host.test/foo", "A=2 A=1 A=3"},
678 | {"http://www.google.com/foo", "A=9 A=6 A=8"},
679 | },
680 | },
681 | {
682 | "Delete A6.",
683 | "http://www.google.com",
684 | []string{"A=; max-age=-1"},
685 | "A=1 A=2 A=3 A=8 A=9",
686 | []query{
687 | {"http://www.host.test/foo", "A=2 A=1 A=3"},
688 | {"http://www.google.com/foo", "A=9 A=8"},
689 | },
690 | },
691 | {
692 | "Delete A3.",
693 | "http://www.host.test",
694 | []string{"A=; domain=host.test; max-age=-1"},
695 | "A=1 A=2 A=8 A=9",
696 | []query{
697 | {"http://www.host.test/foo", "A=2 A=1"},
698 | {"http://www.google.com/foo", "A=9 A=8"},
699 | },
700 | },
701 | {
702 | "No cross-domain delete.",
703 | "http://www.host.test",
704 | []string{
705 | "A=; domain=google.com; max-age=-1",
706 | "A=; path=/foo; domain=google.com; max-age=-1"},
707 | "A=1 A=2 A=8 A=9",
708 | []query{
709 | {"http://www.host.test/foo", "A=2 A=1"},
710 | {"http://www.google.com/foo", "A=9 A=8"},
711 | },
712 | },
713 | {
714 | "Delete A8 and A9.",
715 | "http://www.google.com",
716 | []string{
717 | "A=; domain=google.com; max-age=-1",
718 | "A=; path=/foo; domain=google.com; max-age=-1"},
719 | "A=1 A=2",
720 | []query{
721 | {"http://www.host.test/foo", "A=2 A=1"},
722 | {"http://www.google.com/foo", ""},
723 | },
724 | },
725 | }
726 |
727 | func TestUpdateAndDelete(t *testing.T) {
728 | jar := newTestJar()
729 | for _, test := range updateAndDeleteTests {
730 | test.run(t, jar)
731 | }
732 | }
733 |
734 | func TestExpiration(t *testing.T) {
735 | jar := newTestJar()
736 | jarTest{
737 | "Expiration.",
738 | "http://www.host.test",
739 | []string{
740 | "a=1",
741 | "b=2; max-age=3",
742 | "c=3; " + expiresIn(3),
743 | "d=4; max-age=5",
744 | "e=5; " + expiresIn(5),
745 | "f=6; max-age=100",
746 | },
747 | "a=1 b=2 c=3 d=4 e=5 f=6", // executed at t0 + 1001 ms
748 | []query{
749 | {"http://www.host.test", "a=1 b=2 c=3 d=4 e=5 f=6"}, // t0 + 2002 ms
750 | {"http://www.host.test", "a=1 d=4 e=5 f=6"}, // t0 + 3003 ms
751 | {"http://www.host.test", "a=1 d=4 e=5 f=6"}, // t0 + 4004 ms
752 | {"http://www.host.test", "a=1 f=6"}, // t0 + 5005 ms
753 | {"http://www.host.test", "a=1 f=6"}, // t0 + 6006 ms
754 | },
755 | }.run(t, jar)
756 | }
757 |
758 | //
759 | // Tests derived from Chromium's cookie_store_unittest.h.
760 | //
761 |
762 | // See http://src.chromium.org/viewvc/chrome/trunk/src/net/cookies/cookie_store_unittest.h?revision=159685&content-type=text/plain
763 | // Some of the original tests are in a bad condition (e.g.
764 | // DomainWithTrailingDotTest) or are not RFC 6265 conforming (e.g.
765 | // TestNonDottedAndTLD #1 and #6) and have not been ported.
766 |
767 | // chromiumBasicsTests contains fundamental tests. Each jarTest has to be
768 | // performed on a fresh, empty Jar.
769 | var chromiumBasicsTests = [...]jarTest{
770 | {
771 | "DomainWithTrailingDotTest.",
772 | "http://www.google.com/",
773 | []string{
774 | "a=1; domain=.www.google.com.",
775 | "b=2; domain=.www.google.com.."},
776 | "",
777 | []query{
778 | {"http://www.google.com", ""},
779 | },
780 | },
781 | {
782 | "ValidSubdomainTest #1.",
783 | "http://a.b.c.d.com",
784 | []string{
785 | "a=1; domain=.a.b.c.d.com",
786 | "b=2; domain=.b.c.d.com",
787 | "c=3; domain=.c.d.com",
788 | "d=4; domain=.d.com"},
789 | "a=1 b=2 c=3 d=4",
790 | []query{
791 | {"http://a.b.c.d.com", "a=1 b=2 c=3 d=4"},
792 | {"http://b.c.d.com", "b=2 c=3 d=4"},
793 | {"http://c.d.com", "c=3 d=4"},
794 | {"http://d.com", "d=4"},
795 | },
796 | },
797 | {
798 | "ValidSubdomainTest #2.",
799 | "http://a.b.c.d.com",
800 | []string{
801 | "a=1; domain=.a.b.c.d.com",
802 | "b=2; domain=.b.c.d.com",
803 | "c=3; domain=.c.d.com",
804 | "d=4; domain=.d.com",
805 | "X=bcd; domain=.b.c.d.com",
806 | "X=cd; domain=.c.d.com"},
807 | "X=bcd X=cd a=1 b=2 c=3 d=4",
808 | []query{
809 | {"http://b.c.d.com", "b=2 c=3 d=4 X=bcd X=cd"},
810 | {"http://c.d.com", "c=3 d=4 X=cd"},
811 | },
812 | },
813 | {
814 | "InvalidDomainTest #1.",
815 | "http://foo.bar.com",
816 | []string{
817 | "a=1; domain=.yo.foo.bar.com",
818 | "b=2; domain=.foo.com",
819 | "c=3; domain=.bar.foo.com",
820 | "d=4; domain=.foo.bar.com.net",
821 | "e=5; domain=ar.com",
822 | "f=6; domain=.",
823 | "g=7; domain=/",
824 | "h=8; domain=http://foo.bar.com",
825 | "i=9; domain=..foo.bar.com",
826 | "j=10; domain=..bar.com",
827 | "k=11; domain=.foo.bar.com?blah",
828 | "l=12; domain=.foo.bar.com/blah",
829 | "m=12; domain=.foo.bar.com:80",
830 | "n=14; domain=.foo.bar.com:",
831 | "o=15; domain=.foo.bar.com#sup",
832 | },
833 | "", // Jar is empty.
834 | []query{{"http://foo.bar.com", ""}},
835 | },
836 | {
837 | "InvalidDomainTest #2.",
838 | "http://foo.com.com",
839 | []string{"a=1; domain=.foo.com.com.com"},
840 | "",
841 | []query{{"http://foo.bar.com", ""}},
842 | },
843 | {
844 | "DomainWithoutLeadingDotTest #1.",
845 | "http://manage.hosted.filefront.com",
846 | []string{"a=1; domain=filefront.com"},
847 | "a=1",
848 | []query{{"http://www.filefront.com", "a=1"}},
849 | },
850 | {
851 | "DomainWithoutLeadingDotTest #2.",
852 | "http://www.google.com",
853 | []string{"a=1; domain=www.google.com"},
854 | "a=1",
855 | []query{
856 | {"http://www.google.com", "a=1"},
857 | {"http://sub.www.google.com", "a=1"},
858 | {"http://something-else.com", ""},
859 | },
860 | },
861 | {
862 | "CaseInsensitiveDomainTest.",
863 | "http://www.google.com",
864 | []string{
865 | "a=1; domain=.GOOGLE.COM",
866 | "b=2; domain=.www.gOOgLE.coM"},
867 | "a=1 b=2",
868 | []query{{"http://www.google.com", "a=1 b=2"}},
869 | },
870 | {
871 | "TestIpAddress #1.",
872 | "http://1.2.3.4/foo",
873 | []string{"a=1; path=/"},
874 | "a=1",
875 | []query{{"http://1.2.3.4/foo", "a=1"}},
876 | },
877 | {
878 | "TestIpAddress #2.",
879 | "http://1.2.3.4/foo",
880 | []string{
881 | "a=1; domain=.1.2.3.4",
882 | "b=2; domain=.3.4"},
883 | "",
884 | []query{{"http://1.2.3.4/foo", ""}},
885 | },
886 | {
887 | "TestIpAddress #3.",
888 | "http://1.2.3.4/foo",
889 | []string{"a=1; domain=1.2.3.4"},
890 | "",
891 | []query{{"http://1.2.3.4/foo", ""}},
892 | },
893 | {
894 | "TestNonDottedAndTLD #2.",
895 | "http://com./index.html",
896 | []string{"a=1"},
897 | "a=1",
898 | []query{
899 | {"http://com./index.html", "a=1"},
900 | {"http://no-cookies.com./index.html", ""},
901 | },
902 | },
903 | {
904 | "TestNonDottedAndTLD #3.",
905 | "http://a.b",
906 | []string{
907 | "a=1; domain=.b",
908 | "b=2; domain=b"},
909 | "",
910 | []query{{"http://bar.foo", ""}},
911 | },
912 | {
913 | "TestNonDottedAndTLD #4.",
914 | "http://google.com",
915 | []string{
916 | "a=1; domain=.com",
917 | "b=2; domain=com"},
918 | "",
919 | []query{{"http://google.com", ""}},
920 | },
921 | {
922 | "TestNonDottedAndTLD #5.",
923 | "http://google.co.uk",
924 | []string{
925 | "a=1; domain=.co.uk",
926 | "b=2; domain=.uk"},
927 | "",
928 | []query{
929 | {"http://google.co.uk", ""},
930 | {"http://else.co.com", ""},
931 | {"http://else.uk", ""},
932 | },
933 | },
934 | {
935 | "TestHostEndsWithDot.",
936 | "http://www.google.com",
937 | []string{
938 | "a=1",
939 | "b=2; domain=.www.google.com."},
940 | "a=1",
941 | []query{{"http://www.google.com", "a=1"}},
942 | },
943 | {
944 | "PathTest",
945 | "http://www.google.izzle",
946 | []string{"a=1; path=/wee"},
947 | "a=1",
948 | []query{
949 | {"http://www.google.izzle/wee", "a=1"},
950 | {"http://www.google.izzle/wee/", "a=1"},
951 | {"http://www.google.izzle/wee/war", "a=1"},
952 | {"http://www.google.izzle/wee/war/more/more", "a=1"},
953 | {"http://www.google.izzle/weehee", ""},
954 | {"http://www.google.izzle/", ""},
955 | },
956 | },
957 | }
958 |
959 | func TestChromiumBasics(t *testing.T) {
960 | for _, test := range chromiumBasicsTests {
961 | jar := newTestJar()
962 | test.run(t, jar)
963 | }
964 | }
965 |
966 | // chromiumDomainTests contains jarTests which must be executed all on the
967 | // same Jar.
968 | var chromiumDomainTests = [...]jarTest{
969 | {
970 | "Fill #1.",
971 | "http://www.google.izzle",
972 | []string{"A=B"},
973 | "A=B",
974 | []query{{"http://www.google.izzle", "A=B"}},
975 | },
976 | {
977 | "Fill #2.",
978 | "http://www.google.izzle",
979 | []string{"C=D; domain=.google.izzle"},
980 | "A=B C=D",
981 | []query{{"http://www.google.izzle", "A=B C=D"}},
982 | },
983 | {
984 | "Verify A is a host cookie and not accessible from subdomain.",
985 | "http://unused.nil",
986 | []string{},
987 | "A=B C=D",
988 | []query{{"http://foo.www.google.izzle", "C=D"}},
989 | },
990 | {
991 | "Verify domain cookies are found on proper domain.",
992 | "http://www.google.izzle",
993 | []string{"E=F; domain=.www.google.izzle"},
994 | "A=B C=D E=F",
995 | []query{{"http://www.google.izzle", "A=B C=D E=F"}},
996 | },
997 | {
998 | "Leading dots in domain attributes are optional.",
999 | "http://www.google.izzle",
1000 | []string{"G=H; domain=www.google.izzle"},
1001 | "A=B C=D E=F G=H",
1002 | []query{{"http://www.google.izzle", "A=B C=D E=F G=H"}},
1003 | },
1004 | {
1005 | "Verify domain enforcement works #1.",
1006 | "http://www.google.izzle",
1007 | []string{"K=L; domain=.bar.www.google.izzle"},
1008 | "A=B C=D E=F G=H",
1009 | []query{{"http://bar.www.google.izzle", "C=D E=F G=H"}},
1010 | },
1011 | {
1012 | "Verify domain enforcement works #2.",
1013 | "http://unused.nil",
1014 | []string{},
1015 | "A=B C=D E=F G=H",
1016 | []query{{"http://www.google.izzle", "A=B C=D E=F G=H"}},
1017 | },
1018 | }
1019 |
1020 | func TestChromiumDomain(t *testing.T) {
1021 | jar := newTestJar()
1022 | for _, test := range chromiumDomainTests {
1023 | test.run(t, jar)
1024 | }
1025 |
1026 | }
1027 |
1028 | // chromiumDeletionTests must be performed all on the same Jar.
1029 | var chromiumDeletionTests = [...]jarTest{
1030 | {
1031 | "Create session cookie a1.",
1032 | "http://www.google.com",
1033 | []string{"a=1"},
1034 | "a=1",
1035 | []query{{"http://www.google.com", "a=1"}},
1036 | },
1037 | {
1038 | "Delete sc a1 via MaxAge.",
1039 | "http://www.google.com",
1040 | []string{"a=1; max-age=-1"},
1041 | "",
1042 | []query{{"http://www.google.com", ""}},
1043 | },
1044 | {
1045 | "Create session cookie b2.",
1046 | "http://www.google.com",
1047 | []string{"b=2"},
1048 | "b=2",
1049 | []query{{"http://www.google.com", "b=2"}},
1050 | },
1051 | {
1052 | "Delete sc b2 via Expires.",
1053 | "http://www.google.com",
1054 | []string{"b=2; " + expiresIn(-10)},
1055 | "",
1056 | []query{{"http://www.google.com", ""}},
1057 | },
1058 | {
1059 | "Create persistent cookie c3.",
1060 | "http://www.google.com",
1061 | []string{"c=3; max-age=3600"},
1062 | "c=3",
1063 | []query{{"http://www.google.com", "c=3"}},
1064 | },
1065 | {
1066 | "Delete pc c3 via MaxAge.",
1067 | "http://www.google.com",
1068 | []string{"c=3; max-age=-1"},
1069 | "",
1070 | []query{{"http://www.google.com", ""}},
1071 | },
1072 | {
1073 | "Create persistent cookie d4.",
1074 | "http://www.google.com",
1075 | []string{"d=4; max-age=3600"},
1076 | "d=4",
1077 | []query{{"http://www.google.com", "d=4"}},
1078 | },
1079 | {
1080 | "Delete pc d4 via Expires.",
1081 | "http://www.google.com",
1082 | []string{"d=4; " + expiresIn(-10)},
1083 | "",
1084 | []query{{"http://www.google.com", ""}},
1085 | },
1086 | }
1087 |
1088 | func TestChromiumDeletion(t *testing.T) {
1089 | jar := newTestJar()
1090 | for _, test := range chromiumDeletionTests {
1091 | test.run(t, jar)
1092 | }
1093 | }
1094 |
1095 | // domainHandlingTests tests and documents the rules for domain handling.
1096 | // Each test must be performed on an empty new Jar.
1097 | var domainHandlingTests = [...]jarTest{
1098 | {
1099 | "Host cookie",
1100 | "http://www.host.test",
1101 | []string{"a=1"},
1102 | "a=1",
1103 | []query{
1104 | {"http://www.host.test", "a=1"},
1105 | {"http://host.test", ""},
1106 | {"http://bar.host.test", ""},
1107 | {"http://foo.www.host.test", ""},
1108 | {"http://other.test", ""},
1109 | {"http://test", ""},
1110 | },
1111 | },
1112 | {
1113 | "Domain cookie #1",
1114 | "http://www.host.test",
1115 | []string{"a=1; domain=host.test"},
1116 | "a=1",
1117 | []query{
1118 | {"http://www.host.test", "a=1"},
1119 | {"http://host.test", "a=1"},
1120 | {"http://bar.host.test", "a=1"},
1121 | {"http://foo.www.host.test", "a=1"},
1122 | {"http://other.test", ""},
1123 | {"http://test", ""},
1124 | },
1125 | },
1126 | {
1127 | "Domain cookie #2",
1128 | "http://www.host.test",
1129 | []string{"a=1; domain=.host.test"},
1130 | "a=1",
1131 | []query{
1132 | {"http://www.host.test", "a=1"},
1133 | {"http://host.test", "a=1"},
1134 | {"http://bar.host.test", "a=1"},
1135 | {"http://foo.www.host.test", "a=1"},
1136 | {"http://other.test", ""},
1137 | {"http://test", ""},
1138 | },
1139 | },
1140 | {
1141 | "Host cookie on IDNA domain #1",
1142 | "http://www.bücher.test",
1143 | []string{"a=1"},
1144 | "a=1",
1145 | []query{
1146 | {"http://www.bücher.test", "a=1"},
1147 | {"http://www.xn--bcher-kva.test", "a=1"},
1148 | {"http://bücher.test", ""},
1149 | {"http://xn--bcher-kva.test", ""},
1150 | {"http://bar.bücher.test", ""},
1151 | {"http://bar.xn--bcher-kva.test", ""},
1152 | {"http://foo.www.bücher.test", ""},
1153 | {"http://foo.www.xn--bcher-kva.test", ""},
1154 | {"http://other.test", ""},
1155 | {"http://test", ""},
1156 | },
1157 | },
1158 | {
1159 | "Host cookie on IDNA domain #2",
1160 | "http://www.xn--bcher-kva.test",
1161 | []string{"a=1"},
1162 | "a=1",
1163 | []query{
1164 | {"http://www.bücher.test", "a=1"},
1165 | {"http://www.xn--bcher-kva.test", "a=1"},
1166 | {"http://bücher.test", ""},
1167 | {"http://xn--bcher-kva.test", ""},
1168 | {"http://bar.bücher.test", ""},
1169 | {"http://bar.xn--bcher-kva.test", ""},
1170 | {"http://foo.www.bücher.test", ""},
1171 | {"http://foo.www.xn--bcher-kva.test", ""},
1172 | {"http://other.test", ""},
1173 | {"http://test", ""},
1174 | },
1175 | },
1176 | {
1177 | "Domain cookie on IDNA domain #1",
1178 | "http://www.bücher.test",
1179 | []string{"a=1; domain=xn--bcher-kva.test"},
1180 | "a=1",
1181 | []query{
1182 | {"http://www.bücher.test", "a=1"},
1183 | {"http://www.xn--bcher-kva.test", "a=1"},
1184 | {"http://bücher.test", "a=1"},
1185 | {"http://xn--bcher-kva.test", "a=1"},
1186 | {"http://bar.bücher.test", "a=1"},
1187 | {"http://bar.xn--bcher-kva.test", "a=1"},
1188 | {"http://foo.www.bücher.test", "a=1"},
1189 | {"http://foo.www.xn--bcher-kva.test", "a=1"},
1190 | {"http://other.test", ""},
1191 | {"http://test", ""},
1192 | },
1193 | },
1194 | {
1195 | "Domain cookie on IDNA domain #2",
1196 | "http://www.xn--bcher-kva.test",
1197 | []string{"a=1; domain=xn--bcher-kva.test"},
1198 | "a=1",
1199 | []query{
1200 | {"http://www.bücher.test", "a=1"},
1201 | {"http://www.xn--bcher-kva.test", "a=1"},
1202 | {"http://bücher.test", "a=1"},
1203 | {"http://xn--bcher-kva.test", "a=1"},
1204 | {"http://bar.bücher.test", "a=1"},
1205 | {"http://bar.xn--bcher-kva.test", "a=1"},
1206 | {"http://foo.www.bücher.test", "a=1"},
1207 | {"http://foo.www.xn--bcher-kva.test", "a=1"},
1208 | {"http://other.test", ""},
1209 | {"http://test", ""},
1210 | },
1211 | },
1212 | {
1213 | "Host cookie on TLD.",
1214 | "http://com",
1215 | []string{"a=1"},
1216 | "a=1",
1217 | []query{
1218 | {"http://com", "a=1"},
1219 | {"http://any.com", ""},
1220 | {"http://any.test", ""},
1221 | },
1222 | },
1223 | {
1224 | "Domain cookie on TLD becomes a host cookie.",
1225 | "http://com",
1226 | []string{"a=1; domain=com"},
1227 | "a=1",
1228 | []query{
1229 | {"http://com", "a=1"},
1230 | {"http://any.com", ""},
1231 | {"http://any.test", ""},
1232 | },
1233 | },
1234 | {
1235 | "Host cookie on public suffix.",
1236 | "http://co.uk",
1237 | []string{"a=1"},
1238 | "a=1",
1239 | []query{
1240 | {"http://co.uk", "a=1"},
1241 | {"http://uk", ""},
1242 | {"http://some.co.uk", ""},
1243 | {"http://foo.some.co.uk", ""},
1244 | {"http://any.uk", ""},
1245 | },
1246 | },
1247 | {
1248 | "Domain cookie on public suffix is ignored.",
1249 | "http://some.co.uk",
1250 | []string{"a=1; domain=co.uk"},
1251 | "",
1252 | []query{
1253 | {"http://co.uk", ""},
1254 | {"http://uk", ""},
1255 | {"http://some.co.uk", ""},
1256 | {"http://foo.some.co.uk", ""},
1257 | {"http://any.uk", ""},
1258 | },
1259 | },
1260 | }
1261 |
1262 | func TestDomainHandling(t *testing.T) {
1263 | for _, test := range domainHandlingTests {
1264 | jar := newTestJar()
1265 | test.run(t, jar)
1266 | }
1267 | }
1268 |
--------------------------------------------------------------------------------