Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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の形式に加え、ExpiresMax-AgeDomainPathSecureHttpOnlySameSiteなどの属性を含めることができます。
  • 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.comco.ukに対してクッキーを設定することを防ぐために使用されます。もしこれが許可されると、悪意のあるサイトが他のサイトのクッキーを盗むことが可能になってしまいます。cookiejarパッケージは、このリストを利用して、クッキーのドメイン属性が不正に設定されていないかを検証します。

Goにおけるテーブル駆動テスト (Table Driven Tests)

Go言語では、テストケースを構造体のスライスとして定義し、ループで各テストケースを実行する「テーブル駆動テスト」というパターンが広く用いられます。この方法は、以下のような利点があります。

  • 可読性: 各テストケースが明確に定義され、入力と期待される出力が一目でわかります。
  • 保守性: 新しいテストケースの追加や既存のテストケースの変更が容易です。
  • 簡潔性: テストロジックの重複を避けることができます。

このコミットのテストコードでは、jarTestqueryといった構造体を用いて、このテーブル駆動テストのパターンが採用されています。

技術的詳細

このコミットは、exp/cookiejarパッケージのJar型にSetCookiesメソッドの具体的なロジックを追加し、クッキーのライフサイクル管理と属性処理をRFC 6265に準拠させることを目的としています。

SetCookiesメソッドのロジック

SetCookiesメソッドは、特定のURL (u) から受信したhttp.Cookieのスライス (cookies) を受け取り、それらをクッキージャーの内部マップ (j.entries) に保存します。

  1. ホストの正規化: まず、受信元のURLのホスト名がjarKey関数によって正規化され、クッキージャーのキーとして使用されます。
  2. デフォルトパスの決定: defaultPath関数を使用して、RFC 6265セクション5.1.4に従って、クッキーのデフォルトパスが決定されます。これは、Set-CookieヘッダーにPath属性が指定されていない場合に適用されます。
  3. クッキーの処理ループ: 受信した各http.Cookieについて、以下の処理が行われます。
    • newEntryの呼び出し: newEntryヘルパー関数が呼び出され、http.Cookieオブジェクトから内部表現であるentry構造体が生成されます。この関数は、クッキーの有効期限、パス、ドメイン、セキュア/HttpOnly属性などを処理します。
    • クッキーの削除: newEntryremoveフラグをtrueで返した場合(例: Max-Ageが負の値であるか、Expiresが過去の日付である場合)、既存のクッキーがジャーから削除されます。
    • クッキーの追加/更新: removeフラグがfalseの場合、クッキーはジャーに追加または更新されます。
      • 既存のクッキーがある場合、そのCreationタイムスタンプが保持されます。
      • 新しいクッキーの場合、現在の時刻がCreationタイムスタンプとして設定されます。
      • LastAccessタイムスタンプは常に現在の時刻に更新されます。
      • now = now.Add(1 * time.Nanosecond): CreationLastAccessのタイムスタンプが厳密に単調増加するように、現在の時刻に1ナノ秒が加算されます。これは、ソート時の決定論的な振る舞いを保証するためです。

entry構造体とid()メソッド

entry構造体は、クッキーの内部表現です。このコミットでは、entryid()メソッドが追加されました。

  • 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属性が指定されていない場合、defPathdefaultPathで計算された値)を使用します。
    • domainAndTypeを呼び出して、クッキーのドメインとHostOnly属性を決定します。
    • MaxAge属性が負の値の場合、クッキーは削除されるべきと判断し、remove = trueを返します。
    • MaxAgeが正の値の場合、Expiresを現在時刻からMaxAge秒後に設定し、Persistent = trueとします。
    • Expiresが指定されている場合、それが過去の日付であればremove = trueを返します。そうでなければExpiresを設定し、Persistent = trueとします。
    • ExpiresMaxAgeも指定されていない場合、クッキーはセッションクッキーと見なされ、ExpiresendOfTime(遠い未来の日付)に設定され、Persistent = falseとされます。
    • SecureHttpOnly属性も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)を返します。ただし、hostdomainが完全に一致し、かつdomainがパブリックサフィックスである場合は、ホストオンリークッキーとして扱われる例外があります。
    • 最後に、hostdomainをドメインマッチするかどうかを検証します。例えば、www.example.comexample.comをドメインマッチしますが、other.comはドメインマッチしません。不正な場合はエラー(errIllegalDomain)を返します。

テストインフラストラクチャ

jar_test.goには、SetCookiesCookiesメソッドを網羅的にテストするための堅牢なテーブル駆動テストフレームワークが導入されました。

  • newTestJar(): testPSL(簡易的なパブリックサフィックスリストの実装)を使用して新しいJarインスタンスを作成するヘルパー関数。
  • defaultPathTests / TestDefaultPath: defaultPath関数の動作を検証するテスト。
  • domainAndTypeTests / TestDomainAndType: domainAndType関数の複雑なロジック(ホストオンリー、ドメインマッチング、エラーケース、PSLの考慮)を検証するテスト。
  • jarTest構造体: SetCookiesCookiesのテストケースをカプセル化するための構造体。
    • 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構造体で定義されたテストケースを実行するメソッド。
    1. fromURLからクッキーをパースし、jar.SetCookiesを呼び出します。
    2. jar.content()(ジャーの現在のクッキー内容を文字列で返すヘルパー)を呼び出し、test.contentと比較して、SetCookiesが正しく動作したことを検証します。
    3. 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構造体を生成します。
  • newEntryremoveフラグを返した場合(クッキーが期限切れなどにより削除されるべき場合)、ジャーから該当するクッキーを削除します。
  • クッキーが追加または更新される場合、id()メソッドで生成されたIDをキーとしてジャーのマップに保存します。
  • Creationタイムスタンプは、既存のクッキーの場合は保持され、新規クッキーの場合は現在の時刻が設定されます。LastAccessは常に現在の時刻に更新されます。
  • now = now.Add(1 * time.Nanosecond)は、CreationLastAccessのタイムスタンプが厳密に単調増加することを保証し、ソート時の決定論的な振る舞いを強制します。これは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属性が指定されている場合、それが過去の日付であれば削除すべきと判断します。そうでなければ、その日付を有効期限とし、永続クッキーとします。
  • ExpiresMaxAgeも指定されていない場合、クッキーはセッションクッキーと見なされ、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.comexample.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)
}

これらは、テーブル駆動テストの各テストケースを定義するための構造体です。jarTestSetCookiesの動作と、その後のCookiesメソッドの動作をまとめてテストするための包括的な情報を含みます。queryCookiesメソッドの単一の呼び出しとその期待される結果を定義します。

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構造体のインスタンスを受け取り、そのテストケースを実行するメソッドです。

  1. test.fromURLtest.setCookiesを使用してjar.SetCookiesを呼び出し、ジャーにクッキーを設定します。
  2. jar.content()を呼び出してジャーの現在の内容を取得し、test.content(期待される内容)と比較して、SetCookiesが正しく動作したことを検証します。
  3. 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で定義された様々なシナリオが含まれています。各テストケースは、SetCookiesCookiesメソッドの期待される振る舞いを明確に示しています。

関連リンク

参考にした情報源リンク