├── 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 |
[1]お知らせ
111 |
[2]レート
112 |
[3]チャート
113 |
[4]取引
114 |
[5]ニュース
115 |
[6]照会
116 |
[7]入出金
117 |
[8]設定
118 |
[9]小林芳彦のマーケットナビ
119 |
[0]メインメニュー
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 |
152 | [#]次へ 153 |
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 |
238 |
60
239 |
240 |
評価損益:
241 |
242 | -7,050 243 |
244 | 245 |
246 |
247 | 成行決済  248 | 指値(逆指)決済 249 |
250 |
251 | OCO決済 252 |
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 |
231 | [#]次へ 232 |
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 |
307 | OCO2 308 |
309 |
両建:
310 |
311 | なし 312 |
313 |
決済順序:
314 |
315 | --- 316 |
317 | 318 |
決済オプション:
319 |
320 | --- 321 |
322 |
売買:
323 |
324 | 売 325 |
326 |
注文区分:
327 |
328 | 指定決済 329 |
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 | --------------------------------------------------------------------------------