Node.js + Jestで「SecurityError:~」が発生した際の解決法

こんなエラーが出た

> jest

 FAIL  test\fileA.test.js
  ● Test suite failed to run

    SecurityError: localStorage is not available for opaque origins
      
      at Window.get localStorage [as localStorage] (node_modules/jsdom/lib/jsdom/browser/Window.js:257:15)
          at Array.forEach (<anonymous>)

突然出たエラー。これまでと特に設定は変えてない。なぜ?(。´・ω・)

見つけたページ

https://qiita.com/supaiku2452/items/972eb7e03414c695d033

でも` jest.config.js `というファイルはない。どこに書けば良いかちょっと迷った。

最終的な解決方法

https://github.com/facebook/jest/issues/6769
https://doc.ebichu.cc/jest/docs/ja/configuration.html

package.jsonに書けばいいらしい。
既に存在するルートディレクトリのpackage.jsonに "jest" を追加する。

{
  "name": "my_application_name",
  "version": "1.0.0",
  "description": "my application description",
  "jest": {
    "verbose": true,
    "testURL": "http://localhost/"
  },
  "main": "index.js",
  
  ...

適当なところに追記しました。
これでJestを再実行するとテストが通った。

解決!

Puppeteer入りNode.jsアプリをServerlessでAWS Lambdaにデプロイするときに困ったこと

結論

nodejs+puppeteer+lambdaのアプリケーションには .npmrc が必須

起きたこと

C:\[hogehoge]>npx sls deploy

をしたら

  Serverless Error ---------------------------------------

  An error occurred: [Function Name] - Unzipped size must be smaller than 262144000 bytes (Service: AWSLambda; Status Code: 400; Error Code: InvalidParameterValueException; Request ID: [fugafuga]).

と怒られました。
Serverlessのログを見てみると、

~略~
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (157.42 MB)...
Serverless: Validating template...
~略~

157MBのzipをアップロードしようとしている。
Lambdaの容量制限は50MBなのでそりゃ怒られる…

調査したこと

ディレクトリのサイズを調査すると、node-module で300MBくらい食ってることが判明。
何度か npm install を繰り返して原因究明。
アプリケーションに使用していたPuppeteerの中にChromium(130MBくらい)が含まれていた。こいつのせい!
こいつがデプロイの時に含まれないようにしたいと思い、除外設定を探すもヒットせず。

解決法

たどり着いた神記事がこちら
https://www.pressmantech.com/tech/serverless/4613

ちょっと自分の対応の角度はズレてた。
解決法は「デプロイに含めない」ではなく、「そもそもインストールしない」だった。

というわけで、↑の記事に従って以下の手順を踏んだ。


1. node-moduleディレクトリを全削除

2. .npmrcをルートディレクトリに作る(書くのは1行だけ)

PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=TRUE

3. npm install

これで skip install が出た。
もう一回デプロイ挑戦したらzipのサイズは130MBくらい減っていて、無事lambdaにアップロードされていった。
ありがたや。

Djangoで空文字・Noneを許容する文字列Validateを作る

やりたいこと

DjangoのSerializerで、
空文字とNoneと、ついでに未入力も許容する文字列バリデータをつくる

結論

CharFieldを定義するときに、
requiredだけでなく allow_blank, allow_null も設定しよう(でも罠がある)

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False, allow_blank=True, allow_null=True)
>>> serializer = TestSerializer(data={})
>>> serializer.is_valid()
True
>>> serializer2 = TestSerializer(data={'hoge': ''})
>>> serializer.is_valid()
True
>>> serializer3 = TestSerializer(data={'hoge': None})
>>> serializer.is_valid()
True

やってみること

テストのためにシリアライザを用意しました。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField()

ここから ""(空文字)、None、そもそも値がない の3パターンが通るようにしていきます。

今の状態でバリデーションをかけてみる
>>> from fuga.serializers import TestSerializer
>>> serializer = TestSerializer(data={})
>>> serializer.is_valid()
False
>>> serializer = TestSerializer(data={'hoge': ''})
>>> serializer.is_valid()
False
>>> serializer = TestSerializer(data={'hoge': None})
>>> serializer.is_valid()
False

すべてFalseになります。

試したこと

CharFieldには状態の異なる「空」を許容するオプションが3つあります。
それらを全て許容状態にしていきます。

1. required

required はすべてのフィールドクラスに共通するバリデートで、
”引数がシリアライザに渡されたかどうか”だけを見ます。
どんな値が入っているかはチェックしません。
デフォルトはTrue。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False)
>>> serializer = TestSerializer(data={})
>>> serializer.is_valid()
True

これでまず、そもそも値が存在しない場合がクリア。

2 allow_blank

allow_blank は空文字とNoneを許容するバリデートで、デフォルトはFalseです。
CharFieldでは allow_null が非推奨で、こちらが推奨されています。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(allow_blank=True)
>>> serializer = TestSerializer(data={'hoge': ''})
>>> serializer.is_valid()
True

>>> serializer2 = TestSerializer(data={'hoge': None})
>>> serializer.is_valid()
True
3. allow_null

allow_null はすべてのフィールドクラスに実装されているバリデートです。
nullを許容するか否かだが、CharFieldでは空文字かnullかの判定になります。
デフォルトはFalse。
上で述べたように、CharFieldでは非推奨とされているようです。

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(allow_null=True)
serializer = TestSerializer(data={'hoge': None})
serializer.is_valid()
True

serializer2 = TestSerializer(data={'hoge': ''})
serializer.is_valid()
True

注意点

allow_blankとallow_nullは上で書いたように、
一つずつオプション指定をすると、
空文字とnullの両方を許容する挙動でした。
しかししかし、required=False と組み合わせると、
allow_blankはnullを、allow_nullは空文字を許容しなくなりました。
どういうことなの……

class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False, allow_null=True)
s = TestSerializer(data={'hoge': ''})
s.is_valid()
False
s2 = TestSerializer(data={'hoge': None})
s2.is_valid()
True
class TestSerializer(serializers.Serializer):
    hoge = serializers.CharField(required=False, allow_blank=True)
s = TestSerializer(data={'hoge': ''})
s.is_valid()
True
s2 = TestSerializer(data={'hoge': None})
s2.is_valid()
False


Serializer fields - Django REST framework
では、

It is valid to set both allow_blank=True and allow_null=True, but doing so means that there will be two differing types of empty value permissible for string representations

とあるため、2つセットしたときの挙動のほうが正しそう…?な気がします。
ただ、2つ使う時はバグに気をつけろ的記述もあるため注意したいところ。

上記の挙動を踏まえて、とりあえず今回はallow_blankもallow_nullもセットしましたが、
厳密なチェックを行う場合は注意してください。

感想

required=Falseにすれば、値がないのも空文字もNullも等価な世界線から来たので、
required=Falseにしてるのにバリデートが通らなくて結構苦戦した…
Django触り始めて1か月くらい、知れば知るほどDjangoさんは奥が深い。

しかしなぜオプションの数で挙動が変わるの…?今度調査したい。します。がんばる

香港旅Tips(MTR、女人街、香港ディズニーランド)

目標:一般のかたにも有益なブログになる
 → ひねり出したネタ:海外旅行Tips

(^o^)
これは2018年5月に初めて香港に行った自分の教訓や気付きですので、 すべての方に有益というわけではございません。
海外旅行は事前の下調べが大変重要になります。 他の情報と合わせて吟味してください。 少しでも誰かの役に立てば幸いです。

海外旅行Tips香港編

TL;DR

空港からMTRで街へ出るときのTips

  1. MTRの路線

  2. 空港から出ているMTR路線は”Airport Express”のみ。

  3. Airport Expressの香港行きは停車駅が
    青衣(ツィンイー)、九龍(カオルーン)、香港(ホンコン)
    のみのため、
    空港から一直線に香港ディズニーランドを目指すと、一度青衣まで出る必要がある。
  4. 青衣をすぎると次は九龍なのだが、九龍から旺角など市街地へは2~30分歩くことになる。
    移動が結構難しいので、MTRの路線図は事前に調べておくとよいです。
    Google Mapの経路案内もとても役に立つ!)

  5. MTRのチケット

Airport Expressのチケットは、空港のMTR有人カウンターで買います。
3人以上でチケットを購入すると割引あり。
個別会計にすると割引は適用されません。
友人同士で行くときなどは、個別会計式にせずまとめて購入することをおすすめ。

また、Airport Expressは特別な路線のため、空港ではAirport Expressの駅までのチケットしか買えません。
(つまり、青衣、九龍、香港行きのいずれかになる。)
他の目的地へ行きたい場合は、Airport Expressを降りてから購入することになります。

  1. MTR車内について

コンセントあり。Wifiあり。自由席。
乗車時間は九龍までで約30分。はやいよ!

女人街とか旺角の歩き方

  1. 女人街 買わせる圧がすごい。押しの弱い人は気がついたら買うことになってる。(自分がそうだった)

買わされないために

  • 「説明ありがとう」の意味で「Thank you」は絶対にだめ →「OK!買います!」の意味に取られる →基本は「No」しか言うな

  • 値引き交渉をしろ

    • 頑張ると半額くらいまで下がる
    • つまり…本来の価格は……?
  • 中国っぽいもの以外は極力買うな

    • あなたがいいなと思った洒落た製品はだいたいコピー品です
    • 関税で没収されるよ

とはいえ見てるだけで楽しいのでぜひとも一度行ってみよう

  1. 旺角

  2. 女人街の近く

  3. 渋谷・原宿を圧縮して全体的に高さを加えたような街並み
  4. ブランドショップもあるので、正規品が買えるよ
  5. デパートも多いのでブランド物探しに行くといいかもしれない
  6. 注意:正規品でも日本に持ち込めない海外ブランドがある 関税で没収されることになるので知らないブランドの衝動買いは避ける(SuperDryとか)

香港ディズニーランドTips

  1. 交通
  2. MTRでSunny Bay駅を降りて、Disneyland Resort Lineに乗り換える
  3. 降りたら人の流れに流されてディズニーっぽい道を進むとある
  4. 簡単!

  5. チケット

  6. 事前にオンラインで購入しておくこと推奨
  7. インターネットから事前に購入すると割引あり、フードの無料クーポンなどもつく
  8. ミールバウチャーもおすすめ。

    • 指定レストランでのお食事券。2食or3食分+ワッフルなど軽食がセットになって割引価格で買える。
    • 3 in 1のタイプを購入して高いものと交換していくと、普通購入に比べて約100HKD(1500円くらい)安くなる計算
  9. アトラクション

  10. だいたい5分待ち。トイストーリーエリアだけちょっと長くて30分とか。
  11. アトラクションよりグリーティングのほうが混雑する。
    • グリーティングは18時くらいで終わってしまうのも理由の一つ
    • 特にアイアンマン、ダッフィーは人気で、4~50分待ち。そして一日を通して混雑状況はほとんど変わらない

以上!

Todo: - 思い出したことあったら書き足す - 時間があったら写真もupする - 途中からリスト形式になってるのをいずれなおす

foreachの中で参照渡しのunsetしたときの不思議

PHPで、配列の中の特定の要素を削除するときといえばだいたい、
↓な感じかと思います。

<?php

$a = ['a' => 1, 'b' => 2, 'c' => 3];

foreach($a as $key => $val){
    if($val === 2){
        unset($a[$key]);
    }
}
array(2) {
  ["a"]=>
  int(1)
  ["c"]=>
  int(3)
}


けど、foreachで参照渡しができると知ったので、こんな書き方を試してみた。

<?php

$a = ['a' => 1, 'b' => 2, 'c' => 3];

foreach($a as &$val){
    if($val === 2){
        unset($val);
    }
}

しかし!!!!!!

array(3) {
  ["a"]=>
  int(1)
  ["b"]=>
  int(2)
  ["c"]=>
  &int(3)
}

消えてない!!!!

<?php

$a = ['a' => 1, 'b' => 2, 'c' => 3];

foreach($a as &$val){
    if($val === 2){
        unset($val);
    }
    $val = '^o^';
}

ただ、unsetのifブロックの外で代入すると、

array(3) {
  ["a"]=>
  string(3) "^o^"
  ["b"]=>
  int(2)
  ["c"]=>
  &string(3) "^o^"
}

unsetした要素以外には代入がされた。


なんでこうなるのか、詳しい人に聞いてもわからなかった…。
ストレートに考えると『unsetで参照が解除されただけ』ということなのだろうか。
分かる人教えてください…。

IDをkeyにした配列をarray_mergeしてバグを出しました

やったこと

<?php
$a = array('104' => 'a', '257' => 'b', '245' => 'c');
$b = array('477' => 'd', '502' => 'e', 'f4c' => '600');

var_dump(array_merge($a, $b)); // <1>
var_dump($a + $b); // <2>

けっか

<1>

array(6) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
  [3]=>
  string(1) "d"
  [4]=>
  string(1) "e"
  ["f4c"]=>
  string(3) "600"
}

<2>

array(6) {
  [100]=>
  string(1) "a"
  [200]=>
  string(1) "b"
  [300]=>
  string(1) "c"
  [400]=>
  string(1) "d"
  [500]=>
  string(1) "e"
  ["f4c"]=>
  string(3) "600"
}

既存に機能追加することになった。

array( key => array(データ) , ...)

な配列がもともとあったので、
同じ形式の配列をもう1つ作ってarray_mergeした。

結果、↑<1>の状態になってちゃんと読み込まれてないままリリースされ、土下座したtodayでした。

array_mergeはホント気をつけて使わねばならんという教訓。
ググると配列 + 配列も罠があるらしいことがわかったので、
確実に望んだ形の配列を得たいときは、

foreach(array as $key => $val)

で明示的に処理したほうがいい。



(テストデータに不備があってテスト環境で検出できなかったのも本当に悔しい。
 ってかテスト書けば検出できた。
 次はテスト書かないカルチャーの場所でもテスト書かせてもらうお願いをするなどしたい。)

OneSignalで特定ユーザーにプッシュを送るためのWeb実装

日本語の解説ページがあんまり見当たらなかった…。

 
OneSignalはプッシュ通知を購読登録されたとき、ユーザーを識別するOneSignal player IDなるものを払い出します。
このPlayer IDを指定して通知を飛ばすと、特定のユーザーだけに送ることができます。
このPlayer IDと他の情報を結びつけて、パーソナライズされた通知を送る実装方法のメモです。
Webでの実装方法なのでJavaScriptを扱ってますが、自分はぜんぜんJavaScriptできないのでどうかご容赦!!

初期登録までは↓が大変参考になりました。
qiita.com
 

☆やりかた☆

 1.スニペットを仕込む
OneSignalを登録したときにheadタグ内に貼ってねっていわれたやつを言われたとおりにペースト。

<link rel="manifest" href="/manifest.json" />
<script src="https://cdn.onesignal.com/sdks/OneSignalSDK.js" async=""></script>
<script>
  var OneSignal = window.OneSignal || [];
  OneSignal.push(function() {
    OneSignal.init({
      appId: "<アプリIDここにいれてね>",
    });
  });
</script>

 
ここからはOneSignalのWeb Push SDKのページもみよう!
documentation.onesignal.com

 

2.通知許可状態変更イベントを仕込む
https://documentation.onesignal.com/docs/web-push-sdk#section-callback-event-parameters

OneSignal.push(function() {
  // Occurs when the user's subscription changes to a new value.
  OneSignal.on('subscriptionChange', function (isSubscribed) {
    console.log("The user's subscription state is now:", isSubscribed);
  });
});

subscriptionChangeは、ブラウザの通知許可状態が変化すると発火します。
isSubscribedには許可状態がbooleanで入ります。

許可=true
拒否=false

このイベントは受信拒否されたときにも発火しますので注意。許可されたら発火でゎない。
 

3.Player IDを取得する
https://documentation.onesignal.com/docs/web-push-sdk#section--getuserid-

  OneSignal.getUserId(function(userId) {
    console.log("OneSignal User ID:", userId);
  });

OneSignal.getUserId()でPlayer IDが取得できます。
(User IDっていったりPlayer IDっていったり、公式の呼び方が定まらなくて混乱した)
このIDを自サーバーのAPIなんかに投げてやればOK。
あとはサーバー側でうまくやって他の情報と紐つけましょう。

 

2と3を混ぜるとこんなかんじ。

OneSignal.push(function() {
  // Occurs when the user's subscription changes to a new value.
  OneSignal.on('subscriptionChange', function (isSubscribed) {
    if (isSubscribed) {
      OneSignal.getUserId(function(userId) {
        // ここでAPIとか叩く!!!!
      });
    }
  });
});

 
4.Player IDをターゲティングしてプッシュ通知を送信する

公式に実装サンプルがあるのでこちらをどうぞ。
documentation.onesignal.com

OneSignalは驚くほどにわかりやすい。
PWAとかService Workerとか、高い壁をガンガン叩き壊してくれますね…。
生きやすい世界万歳🙏🙏🙏🙏🙏

ちなみにプッシュ送信のときには、
Segment Tags UserIDのいずれかから1つ指定して送信できます。
複数指定するっていうアホなことをやって軽く詰んだ(みんなに届くジャン・・・)のですが、
Tags < Segment < UserID
の順で優先されるようです。