es9p_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. package device
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "encoding/json"
  7. "net/http"
  8. "net/http/httptest"
  9. "strings"
  10. "testing"
  11. )
  12. // newTestES9P routes an es9pClient at a throwaway TLS server.
  13. func newTestES9P(t *testing.T, handler http.HandlerFunc) *es9pClient {
  14. t.Helper()
  15. server := httptest.NewTLSServer(handler)
  16. t.Cleanup(server.Close)
  17. client := newES9PClient(strings.TrimPrefix(server.URL, "https://"))
  18. client.http = server.Client()
  19. return client
  20. }
  21. func successEnvelope(fields map[string]any) map[string]any {
  22. env := map[string]any{
  23. "header": map[string]any{"functionExecutionStatus": map[string]any{"status": "Executed-Success"}},
  24. }
  25. for key, value := range fields {
  26. env[key] = value
  27. }
  28. return env
  29. }
  30. func b64(value []byte) string { return base64.StdEncoding.EncodeToString(value) }
  31. func TestInitiateAuthenticationSuccess(t *testing.T) {
  32. signed1 := []byte{0x30, 0x03, 0x80, 0x01, 0x09}
  33. client := newTestES9P(t, func(w http.ResponseWriter, r *http.Request) {
  34. if r.URL.Path != "/gsma/rsp2/es9plus/initiateAuthentication" {
  35. t.Errorf("path = %s", r.URL.Path)
  36. }
  37. if r.Header.Get("X-Admin-Protocol") != "gsma/rsp/v2.2.2" {
  38. t.Errorf("X-Admin-Protocol = %q", r.Header.Get("X-Admin-Protocol"))
  39. }
  40. if r.Header.Get("User-Agent") != "gsma-rsp-lpad" {
  41. t.Errorf("User-Agent = %q", r.Header.Get("User-Agent"))
  42. }
  43. var req map[string]string
  44. _ = json.NewDecoder(r.Body).Decode(&req)
  45. if req["smdpAddress"] == "" || req["euiccChallenge"] == "" || req["euiccInfo1"] == "" {
  46. t.Errorf("missing request fields: %v", req)
  47. }
  48. _ = json.NewEncoder(w).Encode(successEnvelope(map[string]any{
  49. "transactionId": "dHJhbnNhY3Rpb24=",
  50. "serverSigned1": b64(signed1),
  51. "serverSignature1": b64([]byte{0x01, 0x02, 0x03}),
  52. "euiccCiPKIdToBeUsed": b64([]byte{0x04, 0x05}),
  53. "serverCertificate": b64([]byte{0x30, 0x01, 0x00}),
  54. }))
  55. })
  56. result, err := client.initiateAuthentication(context.Background(), []byte{0x09, 0x09}, []byte{0x08, 0x08})
  57. if err != nil {
  58. t.Fatalf("initiateAuthentication: %v", err)
  59. }
  60. if result.TransactionID != "dHJhbnNhY3Rpb24=" {
  61. t.Errorf("transactionId = %q", result.TransactionID)
  62. }
  63. if !bytes.Equal(result.ServerSigned1, signed1) {
  64. t.Errorf("serverSigned1 = %X", result.ServerSigned1)
  65. }
  66. if !bytes.Equal(result.EuiccCiPKIDToBeUsed, []byte{0x04, 0x05}) {
  67. t.Errorf("euiccCiPKIdToBeUsed = %X", result.EuiccCiPKIDToBeUsed)
  68. }
  69. }
  70. // A Failed status with a server-supplied message surfaces that message verbatim.
  71. func TestAuthenticateClientFailureMessage(t *testing.T) {
  72. client := newTestES9P(t, func(w http.ResponseWriter, r *http.Request) {
  73. _ = json.NewEncoder(w).Encode(map[string]any{
  74. "header": map[string]any{"functionExecutionStatus": map[string]any{
  75. "status": "Failed",
  76. "statusCodeData": map[string]string{
  77. "subjectCode": "8.2.6", "reasonCode": "3.8", "message": "The matchingID is not found",
  78. },
  79. }},
  80. })
  81. })
  82. _, err := client.authenticateClient(context.Background(), "dA==", []byte{0x01})
  83. if err == nil || err.Error() != "The matchingID is not found" {
  84. t.Fatalf("err = %v", err)
  85. }
  86. if code := ESIMDownloadErrorCode(err); code != "activation_code_refused" {
  87. t.Fatalf("code = %q, want activation_code_refused", code)
  88. }
  89. }
  90. // A Failed status with only codes (no message) falls back to the SGP.22 table,
  91. // and the insufficient-memory pair maps to the SPA's special error code.
  92. func TestGetBoundProfilePackageInsufficientMemory(t *testing.T) {
  93. client := newTestES9P(t, func(w http.ResponseWriter, r *http.Request) {
  94. _ = json.NewEncoder(w).Encode(map[string]any{
  95. "header": map[string]any{"functionExecutionStatus": map[string]any{
  96. "status": "Failed",
  97. "statusCodeData": map[string]string{"subjectCode": "8.1", "reasonCode": "4.8"},
  98. }},
  99. })
  100. })
  101. _, err := client.getBoundProfilePackage(context.Background(), "dA==", []byte{0x01})
  102. if err == nil {
  103. t.Fatalf("expected error")
  104. }
  105. if !strings.Contains(err.Error(), "sufficient space") {
  106. t.Fatalf("err = %v, want table-supplied space message", err)
  107. }
  108. if code := ESIMDownloadErrorCode(err); code != "euicc_insufficient_memory" {
  109. t.Fatalf("code = %q, want euicc_insufficient_memory", code)
  110. }
  111. }
  112. func TestGetBoundProfilePackageSuccess(t *testing.T) {
  113. pkg := []byte{0xBF, 0x36, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05}
  114. client := newTestES9P(t, func(w http.ResponseWriter, r *http.Request) {
  115. if r.URL.Path != "/gsma/rsp2/es9plus/getBoundProfilePackage" {
  116. t.Errorf("path = %s", r.URL.Path)
  117. }
  118. var req map[string]string
  119. _ = json.NewDecoder(r.Body).Decode(&req)
  120. if req["transactionId"] == "" || req["prepareDownloadResponse"] == "" {
  121. t.Errorf("missing request fields: %v", req)
  122. }
  123. _ = json.NewEncoder(w).Encode(successEnvelope(map[string]any{
  124. "boundProfilePackage": b64(pkg),
  125. }))
  126. })
  127. got, err := client.getBoundProfilePackage(context.Background(), "dA==", []byte{0xAA})
  128. if err != nil {
  129. t.Fatalf("getBoundProfilePackage: %v", err)
  130. }
  131. if !bytes.Equal(got, pkg) {
  132. t.Fatalf("bpp = %X, want %X", got, pkg)
  133. }
  134. }