[インデックス 15195] ファイルの概要
このコミットは、Go言語の実験的なexp/cookiejar
パッケージにおいて、HTTPクッキーを管理するためのSetCookies
メソッドの実装を完了し、そのテストインフラストラクチャを導入するものです。特に、RFC 6265で定義されているクッキーの仕様に厳密に従い、ドメイン、パス、有効期限などの複雑なルールを正確に処理するロジックが追加されています。また、読みやすさとレビューのしやすさを重視したテーブル駆動テストの基盤が構築されています。
コミット
commit de69401b7513bd94d01061c01d9c5c8c8dfdfae2
Author: Volker Dobler <dr.volker.dobler@gmail.com>
Date: Mon Feb 11 11:47:31 2013 +1100
exp/cookiejar: implementation of SetCookies
This CL provides the rest of the SetCookies code as well as
some test infrastructure which will be used to test also
the Cookies method. This test infrastructure is optimized
for readability and tries to make it easy to review table
driven test cases.
Tests for all the different corner cases of SetCookies
will be provided in a separate CL.
R=nigeltao, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/7306054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/de69401b7513bd94d01061c01d9c5c8c8dfdfae2
元コミット内容
exp/cookiejar: implementation of SetCookies
This CL provides the rest of the SetCookies code as well as
some test infrastructure which will be used to test also
the Cookies method. This test infrastructure is optimized
for readability and tries to make it easy to review table
driven test cases.
Tests for all the different corner cases of SetCookies
will be provided in a separate CL.
変更の背景
HTTPクッキーは、Webアプリケーションにおいてセッション管理、パーソナライゼーション、トラッキングなどに不可欠な要素です。Go言語の標準ライブラリにはnet/http/cookiejar
パッケージ(このコミット時点ではexp/cookiejar
として実験的に開発中)があり、これはブラウザのようにクッキーを保存・管理する機能を提供します。
このコミットの主な背景は、http.CookieJar
インターフェースの重要なメソッドであるSetCookies
の完全な実装をexp/cookiejar
パッケージに組み込むことでした。SetCookies
は、HTTPレスポンスヘッダーのSet-Cookie
フィールドから受け取ったクッキーを、クッキージャーの内部ストレージに適切に保存・更新する役割を担います。この処理は、RFC 6265で定められた複雑なルール(ドメインマッチング、パスマッチング、有効期限、セキュア属性、HttpOnly属性など)に厳密に従う必要があります。
また、クッキー管理のロジックは多くのエッジケースを持つため、堅牢なテストが不可欠です。このコミットでは、SetCookies
だけでなく、関連するCookies
メソッドの動作も検証するための、読みやすくレビューしやすいテーブル駆動テストの基盤も同時に構築されました。これにより、将来的な機能追加やバグ修正の際に、既存の動作が損なわれないことを保証する安全網が提供されます。
前提知識の解説
HTTP Cookies (RFC 6265)
HTTPクッキーは、サーバーがユーザーエージェント(通常はWebブラウザ)に送信し、ユーザーエージェントがその後のリクエストでサーバーに送り返す小さなデータ片です。これにより、ステートレスなHTTPプロトコル上でセッション状態を維持することが可能になります。
Set-Cookie
ヘッダー: サーバーからクライアントへクッキーを送信するために使用されます。Name=Value
の形式に加え、Expires
、Max-Age
、Domain
、Path
、Secure
、HttpOnly
、SameSite
などの属性を含めることができます。Cookie
ヘッダー: クライアントからサーバーへクッキーを送信するために使用されます。
RFC 6265は、HTTP State Management Mechanismの現在の標準であり、クッキーの生成、保存、送信に関する詳細なルールを定義しています。特に以下の点が重要です。
- ドメイン属性 (Domain Attribute): クッキーが送信されるドメインを制限します。
Domain
属性が指定されない場合、クッキーは設定されたホストにのみ適用される「ホストオンリークッキー」となります。指定された場合、そのドメインとそのサブドメインに適用されます。RFC 6265では、ドメイン属性がIPアドレスである場合や、パブリックサフィックス(例:.com
,.co.uk
)である場合の特別な扱いが定義されています。 - パス属性 (Path Attribute): クッキーが送信されるURLパスを制限します。指定されたパスとそのサブパスにのみ適用されます。
- 有効期限 (Expiration):
Expires
またはMax-Age
属性によってクッキーの有効期限が設定されます。これらが指定されない場合、クッキーはブラウザのセッション終了時に削除される「セッションクッキー」となります。 - セキュア属性 (Secure Attribute): この属性が設定されたクッキーは、HTTPS接続でのみ送信されます。
- HttpOnly属性 (HttpOnly Attribute): この属性が設定されたクッキーは、JavaScriptなどのクライアントサイドスクリプトからアクセスできません。クロスサイトスクリプティング (XSS) 攻撃からの保護に役立ちます。
Public Suffix List (PSL)
パブリックサフィックスリストは、ドメイン名の「パブリックサフィックス」(例: .com
, .co.uk
, .github.io
など)を列挙したリストです。これは、セキュリティ上の理由から非常に重要です。例えば、example.com
がco.uk
に対してクッキーを設定することを防ぐために使用されます。もしこれが許可されると、悪意のあるサイトが他のサイトのクッキーを盗むことが可能になってしまいます。cookiejar
パッケージは、このリストを利用して、クッキーのドメイン属性が不正に設定されていないかを検証します。
Goにおけるテーブル駆動テスト (Table Driven Tests)
Go言語では、テストケースを構造体のスライスとして定義し、ループで各テストケースを実行する「テーブル駆動テスト」というパターンが広く用いられます。この方法は、以下のような利点があります。
- 可読性: 各テストケースが明確に定義され、入力と期待される出力が一目でわかります。
- 保守性: 新しいテストケースの追加や既存のテストケースの変更が容易です。
- 簡潔性: テストロジックの重複を避けることができます。
このコミットのテストコードでは、jarTest
やquery
といった構造体を用いて、このテーブル駆動テストのパターンが採用されています。
技術的詳細
このコミットは、exp/cookiejar
パッケージのJar
型にSetCookies
メソッドの具体的なロジックを追加し、クッキーのライフサイクル管理と属性処理をRFC 6265に準拠させることを目的としています。
SetCookies
メソッドのロジック
SetCookies
メソッドは、特定のURL (u
) から受信したhttp.Cookie
のスライス (cookies
) を受け取り、それらをクッキージャーの内部マップ (j.entries
) に保存します。
- ホストの正規化: まず、受信元のURLのホスト名が
jarKey
関数によって正規化され、クッキージャーのキーとして使用されます。 - デフォルトパスの決定:
defaultPath
関数を使用して、RFC 6265セクション5.1.4に従って、クッキーのデフォルトパスが決定されます。これは、Set-Cookie
ヘッダーにPath
属性が指定されていない場合に適用されます。 - クッキーの処理ループ: 受信した各
http.Cookie
について、以下の処理が行われます。newEntry
の呼び出し:newEntry
ヘルパー関数が呼び出され、http.Cookie
オブジェクトから内部表現であるentry
構造体が生成されます。この関数は、クッキーの有効期限、パス、ドメイン、セキュア/HttpOnly属性などを処理します。- クッキーの削除:
newEntry
がremove
フラグをtrue
で返した場合(例:Max-Age
が負の値であるか、Expires
が過去の日付である場合)、既存のクッキーがジャーから削除されます。 - クッキーの追加/更新:
remove
フラグがfalse
の場合、クッキーはジャーに追加または更新されます。- 既存のクッキーがある場合、その
Creation
タイムスタンプが保持されます。 - 新しいクッキーの場合、現在の時刻が
Creation
タイムスタンプとして設定されます。 LastAccess
タイムスタンプは常に現在の時刻に更新されます。now = now.Add(1 * time.Nanosecond)
:Creation
とLastAccess
のタイムスタンプが厳密に単調増加するように、現在の時刻に1ナノ秒が加算されます。これは、ソート時の決定論的な振る舞いを保証するためです。
- 既存のクッキーがある場合、その
entry
構造体とid()
メソッド
entry
構造体は、クッキーの内部表現です。このコミットでは、entry
にid()
メソッドが追加されました。
func (e *entry) id() string
: クッキーを一意に識別するための文字列(Domain;Path;Name
の形式)を返します。これは、ジャー内でクッキーをマップのキーとして管理するために使用されます。
defaultPath
関数
func defaultPath(path string) string
: RFC 6265セクション5.1.4「Default Path」のルールに従って、URLのパスからクッキーのデフォルトパスを計算します。例えば、/a/b/c.html
からは/a/b
が、/a/b/
からは/a/b
が返されます。パスが空または不正な場合は/
を返します。
newEntry
関数
func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error)
:http.Cookie
オブジェクトからentry
構造体を生成する主要なヘルパー関数です。- クッキーの
Path
属性が指定されていない場合、defPath
(defaultPath
で計算された値)を使用します。 domainAndType
を呼び出して、クッキーのドメインとHostOnly
属性を決定します。MaxAge
属性が負の値の場合、クッキーは削除されるべきと判断し、remove = true
を返します。MaxAge
が正の値の場合、Expires
を現在時刻からMaxAge
秒後に設定し、Persistent = true
とします。Expires
が指定されている場合、それが過去の日付であればremove = true
を返します。そうでなければExpires
を設定し、Persistent = true
とします。Expires
もMaxAge
も指定されていない場合、クッキーはセッションクッキーと見なされ、Expires
はendOfTime
(遠い未来の日付)に設定され、Persistent = false
とされます。Secure
とHttpOnly
属性もentry
にコピーされます。
- クッキーの
domainAndType
関数
func (j *Jar) domainAndType(host, domain string) (string, bool, error)
: クッキーのDomain
属性とHostOnly
属性を決定し、RFC 6265のドメインマッチングルールとパブリックサフィックスリストの制約を適用します。Domain
属性が空の場合、クッキーはホストオンリークッキーとなり、host
がドメインとして使用されます。host
がIPアドレスの場合、RFC 6265の規定によりドメイン属性を持つクッキーは設定できません(errNoHostname
)。Domain
属性が.
で始まる場合、最初の.
を削除します。Domain
属性が空になったり、再度.
で始まる場合、または末尾が.
で終わる場合は不正なドメインとしてエラー(errMalformedDomain
)を返します。- パブリックサフィックスリスト (
j.psList
) が設定されている場合、ドメインがパブリックサフィックス自体であるか、またはパブリックサフィックスのサブドメインでないかをチェックします。不正な場合はエラー(errIllegalDomain
)を返します。ただし、host
とdomain
が完全に一致し、かつdomain
がパブリックサフィックスである場合は、ホストオンリークッキーとして扱われる例外があります。 - 最後に、
host
がdomain
をドメインマッチするかどうかを検証します。例えば、www.example.com
はexample.com
をドメインマッチしますが、other.com
はドメインマッチしません。不正な場合はエラー(errIllegalDomain
)を返します。
テストインフラストラクチャ
jar_test.go
には、SetCookies
とCookies
メソッドを網羅的にテストするための堅牢なテーブル駆動テストフレームワークが導入されました。
newTestJar()
:testPSL
(簡易的なパブリックサフィックスリストの実装)を使用して新しいJar
インスタンスを作成するヘルパー関数。defaultPathTests
/TestDefaultPath
:defaultPath
関数の動作を検証するテスト。domainAndTypeTests
/TestDomainAndType
:domainAndType
関数の複雑なロジック(ホストオンリー、ドメインマッチング、エラーケース、PSLの考慮)を検証するテスト。jarTest
構造体:SetCookies
とCookies
のテストケースをカプセル化するための構造体。description
: テストケースの説明。fromURL
:Set-Cookie
ヘッダーが受信されたURL。setCookies
:Set-Cookie
ヘッダーの文字列スライス。content
:SetCookies
実行後のジャーの期待される内容(name=value
形式の文字列)。queries
:Cookies
メソッドのテストケースのスライス。
query
構造体:Jar.Cookies
メソッドの単一のテストケースを定義。toURL
:Cookies
メソッドに渡されるURL。want
:Cookies
メソッドから返される期待されるクッキーのリスト(順序が重要)。
jarTest.run()
メソッド:jarTest
構造体で定義されたテストケースを実行するメソッド。fromURL
からクッキーをパースし、jar.SetCookies
を呼び出します。jar.content()
(ジャーの現在のクッキー内容を文字列で返すヘルパー)を呼び出し、test.content
と比較して、SetCookies
が正しく動作したことを検証します。queries
内の各query
について、jar.Cookies
を呼び出し、結果をquery.want
と比較します。
basicsTests
:jarTest
の配列で、基本的なクッキーの動作(ホストクッキー、セキュアクッキー、明示的/暗黙的なパス、クッキーのソート順序、同名クッキーの扱いなど)を網羅する多数のテストケースが含まれています。
このテストインフラストラクチャは、Goのテーブル駆動テストのベストプラクティスに従っており、新しいテストケースの追加が容易で、コードの変更が既存の動作に影響を与えないことを保証する上で非常に効果的です。
コアとなるコードの変更箇所
このコミットで主に変更されたファイルは以下の通りです。
src/pkg/exp/cookiejar/jar.go
:SetCookies
メソッドの実装、および関連するヘルパー関数の追加。src/pkg/exp/cookiejar/jar_test.go
:SetCookies
およびCookies
メソッドのテストインフラストラクチャとテストケースの追加。
具体的に追加・変更された主要な関数・メソッドは以下の通りです。
func (e *entry) id() string
(jar.go)func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie)
(jar.go) - 実装の追加func defaultPath(path string) string
(jar.go)func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error)
(jar.go)func (j *Jar) domainAndType(host, domain string) (string, bool, error)
(jar.go)func newTestJar() *Jar
(jar_test.go)func (jar *Jar) content() string
(jar_test.go)type jarTest struct { ... }
(jar_test.go)type query struct { ... }
(jar_test.go)func (test jarTest) run(t *testing.T, jar *Jar)
(jar_test.go)var basicsTests = [...]jarTest{ ... }
(jar_test.go)
コアとなるコードの解説
src/pkg/exp/cookiejar/jar.go
func (e *entry) id() string
// Id returns the domain;path;name triple of e as an id.
func (e *entry) id() string {
return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)
}
このメソッドは、entry
構造体(クッキーの内部表現)から、そのクッキーを一意に識別するための文字列IDを生成します。IDはDomain;Path;Name
の形式で、クッキージャーの内部マップでキーとして使用され、特定のクッキーを効率的に検索・更新・削除するために役立ちます。
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie)
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
// ... (前略) ...
j.mu.Lock()
defer j.mu.Unlock()
submap := j.entries[key]
now := time.Now()
modified := false
for _, cookie := range cookies {
e, remove, err := j.newEntry(cookie, now, defPath, host)
if err != nil {
continue
}
id := e.id()
if remove {
if submap != nil {
if _, ok := submap[id]; ok {
delete(submap, id)
modified = true
}
}
continue
}
if submap == nil {
submap = make(map[string]entry)
}
if old, ok := submap[id]; ok {
e.Creation = old.Creation
} else {
e.Creation = now
}
e.LastAccess = now
submap[id] = e
modified = true
// Make Creation and LastAccess strictly monotonic forcing
// deterministic behaviour during sorting.
// TODO: check if this is conforming to RFC 6265.
now = now.Add(1 * time.Nanosecond)
}
if modified {
j.entries[key] = submap
}
}
このメソッドは、http.CookieJar
インターフェースのSetCookies
メソッドの実装です。指定されたURLから受信したhttp.Cookie
オブジェクトのリストを処理し、クッキージャーに保存します。
- ミューテックス (
j.mu
) を使用して並行アクセスから保護します。 - 各クッキーに対して
newEntry
を呼び出し、内部のentry
構造体を生成します。 newEntry
がremove
フラグを返した場合(クッキーが期限切れなどにより削除されるべき場合)、ジャーから該当するクッキーを削除します。- クッキーが追加または更新される場合、
id()
メソッドで生成されたIDをキーとしてジャーのマップに保存します。 Creation
タイムスタンプは、既存のクッキーの場合は保持され、新規クッキーの場合は現在の時刻が設定されます。LastAccess
は常に現在の時刻に更新されます。now = now.Add(1 * time.Nanosecond)
は、Creation
とLastAccess
のタイムスタンプが厳密に単調増加することを保証し、ソート時の決定論的な振る舞いを強制します。これはRFC 6265への準拠を将来的に確認する必要があるというコメントが付いています。
func defaultPath(path string) string
// defaultPath returns the directory part of an URL's path according to
// RFC 6265 section 5.1.4.
func defaultPath(path string) string {
if len(path) == 0 || path[0] != '/' {
return "/" // Path is empty or malformed.
}
i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.
if i == 0 {
return "/" // Path has the form "/abc".
}
return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".
}
この関数は、RFC 6265セクション5.1.4「Default Path」のルールに従って、URLのパスからクッキーのデフォルトパスを決定します。例えば、/foo/bar/baz.html
のようなパスからは/foo/bar
が、/foo/bar/
のようなパスからは/foo/bar
が返されます。これは、Set-Cookie
ヘッダーにPath
属性が明示的に指定されていない場合に、クッキーが適用されるパスを決定するために使用されます。
func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error)
// newEntry creates an entry from a http.Cookie c. now is the current time and
// is compared to c.Expires to determine deletion of c. defPath and host are the
// default-path and the canonical host name of the URL c was received from.
//
// remove is whether the jar should delete this cookie, as it has already
// expired with respect to now. In this case, e may be incomplete, but it will
// be valid to call e.id (which depends on e's Name, Domain and Path).
//
// A malformed c.Domain will result in an error.
func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {
e.Name = c.Name
if c.Path == "" || c.Path[0] != '/' {
e.Path = defPath
} else {
e.Path = c.Path
}
e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)
if err != nil {
return e, false, err
}
// MaxAge takes precedence over Expires.
if c.MaxAge < 0 {
return e, true, nil
} else if c.MaxAge > 0 {
e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)
e.Persistent = true
} else {
if c.Expires.IsZero() {
e.Expires = endOfTime
e.Persistent = false
} else {
if c.Expires.Before(now) {
return e, true, nil
}
e.Expires = c.Expires
e.Persistent = true
}
}
e.Value = c.Value
e.Secure = c.Secure
e.HttpOnly = c.HttpOnly
return e, false, nil
}
この関数は、http.Cookie
オブジェクトを受け取り、それをクッキージャーの内部表現であるentry
構造体に変換します。
- クッキーの
Path
属性が指定されていない場合、defaultPath
で計算されたパスを使用します。 domainAndType
関数を呼び出して、クッキーのドメインとHostOnly
属性を決定します。MaxAge
属性が負の値の場合、クッキーは即座に削除されるべきと判断し、remove = true
を返します。MaxAge
が正の値の場合、現在の時刻からMaxAge
秒後に有効期限を設定し、永続クッキー (Persistent = true
) とします。Expires
属性が指定されている場合、それが過去の日付であれば削除すべきと判断します。そうでなければ、その日付を有効期限とし、永続クッキーとします。Expires
もMaxAge
も指定されていない場合、クッキーはセッションクッキーと見なされ、endOfTime
(遠い未来の日付)を有効期限とし、非永続クッキー (Persistent = false
) とします。Value
,Secure
,HttpOnly
などの他の属性もentry
にコピーされます。
func (j *Jar) domainAndType(host, domain string) (string, bool, error)
// domainAndType determines the cookie's domain and hostOnly attribute.
func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
if domain == "" {
// No domain attribute in the SetCookie header indicates a
// host cookie.
return host, true, nil
}
if isIP(host) {
// According to RFC 6265 domain-matching includes not being
// an IP address.
// TODO: This might be relaxed as in common browsers.
return "", false, errNoHostname
}
// From here on: If the cookie is valid, it is a domain cookie (with
// the one exception of a public suffix below).
// See RFC 6265 section 5.2.3.
if domain[0] == '.' {
domain = domain[1:]
}
if len(domain) == 0 || domain[0] == '.' {
// Received either "Domain=." or "Domain=..some.thing",
// both are illegal.
return "", false, errMalformedDomain
}
domain = strings.ToLower(domain)
if domain[len(domain)-1] == '.' {
// We received stuff like "Domain=www.example.com.".
// Browsers do handle such stuff (actually differently) but
// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in
// requiring a reject. 4.1.2.3 is not normative, but
// "Domain Matching" (5.1.3) and "Canonicalized Host Names"
// (5.1.2) are.
return "", false, errMalformedDomain
}
// See RFC 6265 section 5.3 #5.
if j.psList != nil {
if ps := j.psList.PublicSuffix(domain); ps != "" && !strings.HasSuffix(domain, "."+ps) {
if host == domain {
// This is the one exception in which a cookie
// with a domain attribute is a host cookie.
return host, true, nil
}
return "", false, errIllegalDomain
}
}
// The domain must domain-match host: www.mycompany.com cannot
// set cookies for .ourcompetitors.com.
if host != domain && !strings.HasSuffix(host, "."+domain) {
return "", false, errIllegalDomain
}
return domain, false, nil
}
この関数は、Set-Cookie
ヘッダーのDomain
属性と、クッキーが受信されたホスト名に基づいて、クッキーの最終的なドメインとHostOnly
属性を決定します。RFC 6265の複雑なルールとセキュリティ上の考慮事項が実装されています。
Domain
属性が指定されていない場合、ホストオンリークッキーとして扱われます。- ホストがIPアドレスの場合、ドメイン属性を持つクッキーは許可されません。
Domain
属性の先頭の.
を削除します。- 不正な形式のドメイン(例:
.
のみ、..something
、末尾に.
がある)はエラーとします。 - パブリックサフィックスリスト (
j.psList
) を使用して、ドメインがパブリックサフィックス自体であるか、またはパブリックサフィックスのサブドメインでないかを検証します。これにより、セキュリティ上の脆弱性を防ぎます。 - 最後に、受信元の
host
が、設定しようとしているdomain
を「ドメインマッチ」するかどうかを検証します。これは、www.example.com
がexample.com
のクッキーを設定できるが、other.com
のクッキーは設定できないというルールです。
src/pkg/exp/cookiejar/jar_test.go
func newTestJar() *Jar
// newTestJar creates an empty Jar with testPSL as the public suffix list.
func newTestJar() *Jar {
jar, err := New(&Options{PublicSuffixList: testPSL{}})
if err != nil {
panic(err)
}
return jar
}
テスト用のJar
インスタンスを簡単に作成するためのヘルパー関数です。簡易的なtestPSL
(パブリックサフィックスリストのテスト実装)を設定して初期化します。
func (jar *Jar) content() string
// content yields the (non-expired) cookies of jar in the form
// "name1=value1 name2=value2 ...".
func (jar *Jar) content() string {
var cookies []string
now := time.Now().UTC()
for _, submap := range jar.entries {
for _, cookie := range submap {
if !cookie.Expires.After(now) {
continue
}
cookies = append(cookies, cookie.Name+"="+cookie.Value)
}
}
sort.Strings(cookies)
return strings.Join(cookies, " ")
}
テストの検証を容易にするためのヘルパーメソッドです。ジャー内に現在保存されている期限切れでないすべてのクッキーを、name=value
形式の文字列としてソートして結合し、返します。これにより、SetCookies
操作後のジャーの内部状態を簡単に比較できます。
type jarTest struct { ... }
と type query struct { ... }
// jarTest encapsulates the following actions on a jar:
// 1. Perform SetCookies with fromURL and the cookies from setCookies.
// 2. Check that the entries in the jar matches content.
// 3. For each query in tests: Check that Cookies with toURL yields the
// cookies in want.
type jarTest struct {
description string // The description of what this test is supposed to test
fromURL string // The full URL of the request from which Set-Cookie headers where received
setCookies []string // All the cookies received from fromURL
content string // The whole (non-expired) content of the jar
queries []query // Queries to test the Jar.Cookies method
}
// query contains one test of the cookies returned from Jar.Cookies.
type query struct {
toURL string // the URL in the Cookies call
want string // the expected list of cookies (order matters)
}
これらは、テーブル駆動テストの各テストケースを定義するための構造体です。jarTest
はSetCookies
の動作と、その後のCookies
メソッドの動作をまとめてテストするための包括的な情報を含みます。query
はCookies
メソッドの単一の呼び出しとその期待される結果を定義します。
func (test jarTest) run(t *testing.T, jar *Jar)
// run runs the jarTest.
func (test jarTest) run(t *testing.T, jar *Jar) {
u := mustParseURL(test.fromURL)
// Populate jar with cookies.
setCookies := make([]*http.Cookie, len(test.setCookies))
for i, cs := range test.setCookies {
cookies := (&http.Response{Header: http.Header{"Set-Cookie": {cs}}}).Cookies()
if len(cookies) != 1 {
panic(fmt.Sprintf("Wrong cookie line %q: %#v", cs, cookies))
}
setCookies[i] = cookies[0]
}
jar.SetCookies(u, setCookies)
// Make sure jar content matches our expectations.
if got := jar.content(); got != test.content {
t.Errorf("Test %q Content\ngot %q\nwant %q",
test.description, got, test.content)
}
// Test different calls to Cookies.
for _, query := range test.queries {
var s []string
for _, c := range jar.Cookies(mustParseURL(query.toURL)) {
s = append(s, c.Name+"="+c.Value)
}
got := strings.Join(s, " ")
if got != query.want {
// TODO: t.Errorf() once Cookies is implemented
}
}
}
jarTest
構造体のインスタンスを受け取り、そのテストケースを実行するメソッドです。
test.fromURL
とtest.setCookies
を使用してjar.SetCookies
を呼び出し、ジャーにクッキーを設定します。jar.content()
を呼び出してジャーの現在の内容を取得し、test.content
(期待される内容)と比較して、SetCookies
が正しく動作したことを検証します。test.queries
内の各query
について、jar.Cookies
を呼び出し、返されたクッキーのリストをquery.want
(期待されるクッキーのリスト)と比較します。この時点ではCookies
メソッドが完全に実装されていないため、t.Errorf()
はコメントアウトされていますが、将来的に実装が完了すれば有効化される予定です。
var basicsTests = [...]jarTest{ ... }
// basicsTests contains fundamental tests. Each jarTest has to be performed on
// a fresh, empty Jar.
var basicsTests = [...]jarTest{
// ... (多数のテストケース) ...
}
jarTest
構造体の配列として定義された、基本的なクッキーの動作を網羅する多数のテストケースです。これには、ホストクッキー、セキュアクッキー、明示的/暗黙的なパス、クッキーのソート順序、同名クッキーの扱いなど、RFC 6265で定義された様々なシナリオが含まれています。各テストケースは、SetCookies
とCookies
メソッドの期待される振る舞いを明確に示しています。
関連リンク
- Go CL 7306054: https://golang.org/cl/7306054
参考にした情報源リンク
- RFC 6265 - HTTP State Management Mechanism: https://datatracker.ietf.org/doc/html/rfc6265
- Public Suffix List: https://publicsuffix.org/
- Goにおけるテーブル駆動テスト (Table Driven Tests in Go):
- A common pattern for Go tests: table-driven tests: https://go.dev/blog/testing
- (日本語) Go言語のテストの書き方: Table Driven Tests: https://zenn.dev/link/articles/go-table-driven-tests (Zennの記事ですが、概念理解に役立ちます)