Compare commits
180
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b855ceddd | ||
|
|
e30d4da455 | ||
|
|
26e6e57949 | ||
|
|
b572966bc4 | ||
|
|
bbfa4f1043 | ||
|
|
9ad4f0b75e | ||
|
|
2093f4ece2 | ||
|
|
7bc7703dc2 | ||
|
|
0ed7452e77 | ||
|
|
366cc19c1b | ||
|
|
c72f30eabb | ||
|
|
235f0dade0 | ||
|
|
9c718cbc5f | ||
|
|
02de92afc7 | ||
|
|
add01455fb | ||
|
|
497f3ce47f | ||
|
|
64188e5713 | ||
|
|
33a6d6da97 | ||
|
|
6410e3be85 | ||
|
|
136d7e8215 | ||
|
|
d3fdd7b0d8 | ||
|
|
329fcc204c | ||
|
|
ba2c7894a8 | ||
|
|
07667c8881 | ||
|
|
68504a70a0 | ||
|
|
32073b3d66 | ||
|
|
d1c1664a38 | ||
|
|
8cbcff98ec | ||
|
|
26f82ad95c | ||
|
|
1b90604b2d | ||
|
|
a03c002df4 | ||
|
|
1ce9415d34 | ||
|
|
cc842901b3 | ||
|
|
23d7e4cdbb | ||
|
|
b48f8fca31 | ||
|
|
a4da93326e | ||
|
|
8bd5c0d163 | ||
|
|
972ab87247 | ||
|
|
d1ea664b41 | ||
|
|
a55fadd53a | ||
|
|
c70a66a1c5 | ||
|
|
8357846818 | ||
|
|
31ea91afb8 | ||
|
|
6616541f65 | ||
|
|
01695822bb | ||
|
|
f0186d1aab | ||
|
|
41a3359085 | ||
|
|
d6cce8df2c | ||
|
|
596861a594 | ||
|
|
385ea800a4 | ||
|
|
27fc339e36 | ||
|
|
61e2173f25 | ||
|
|
13a3fc3d1b | ||
|
|
6a1b515ea9 | ||
|
|
82f27e6b63 | ||
|
|
6ac75031ad | ||
|
|
8b7f1331ee | ||
|
|
099aad8475 | ||
|
|
c1b3fba344 | ||
|
|
d7e9be99f9 | ||
|
|
067a8bcb21 | ||
|
|
f5569a62f1 | ||
|
|
1ab1ed8a82 | ||
|
|
8d95baa70f | ||
|
|
050bce163a | ||
|
|
68b84683d0 | ||
|
|
6fe373d540 | ||
|
|
ba19bcc729 | ||
|
|
f1b65db2c7 | ||
|
|
f5f287ef06 | ||
|
|
f161e6033c | ||
|
|
c86bff8811 | ||
|
|
596b1045bd | ||
|
|
907ca2b590 | ||
|
|
f1b5469b83 | ||
|
|
984fbde540 | ||
|
|
2bdc05dd47 | ||
|
|
d2ea572abf | ||
|
|
8d5c4cd578 | ||
|
|
dd1ba9b518 | ||
|
|
cebe929118 | ||
|
|
3eccf01f1f | ||
|
|
050f1b7941 | ||
|
|
53cc370727 | ||
|
|
363e05d580 | ||
|
|
2ecc80e18c | ||
|
|
0728b28856 | ||
|
|
a6a9270f63 | ||
|
|
372429a9f5 | ||
|
|
4733ad8eb1 | ||
|
|
8e96aa26b4 | ||
|
|
c0e8520d31 | ||
|
|
345f80d90d | ||
|
|
ffcef2fe99 | ||
|
|
e77dc33333 | ||
|
|
5fee19f6c7 | ||
|
|
659c623240 | ||
|
|
695d0ea13a | ||
|
|
0f7ac514fb | ||
|
|
9ad3be2e97 | ||
|
|
2c36fab8aa | ||
|
|
3b7a4b6e52 | ||
|
|
499302c48e | ||
|
|
28fbe76547 | ||
|
|
fabb31275d | ||
|
|
96b75af027 | ||
|
|
dc048ef047 | ||
|
|
d6b2960a4a | ||
|
|
b47b6c4f56 | ||
|
|
75427df7de | ||
|
|
cb98885e1f | ||
|
|
7e0445cc94 | ||
|
|
a9836aaea0 | ||
|
|
27b28dba0d | ||
|
|
86d059bf99 | ||
|
|
76f5ce32ca | ||
|
|
ff9eab998a | ||
|
|
ac3a36eb13 | ||
|
|
4b4c749756 | ||
|
|
4777f4059f | ||
|
|
ad5d555ac8 | ||
|
|
fc7540fc9b | ||
|
|
63a2bdea48 | ||
|
|
41d47fdb3e | ||
|
|
86dae37c5d | ||
|
|
2a97d9379e | ||
|
|
ba0060316c | ||
|
|
41f18a23e5 | ||
|
|
bf26ad4acc | ||
|
|
acc56ad42b | ||
|
|
922e779118 | ||
|
|
f104a6e1b6 | ||
|
|
0aa6052eba | ||
|
|
f25b34d5e2 | ||
|
|
5e5b3e0830 | ||
|
|
f1163139d7 | ||
|
|
5db6e4364a | ||
|
|
3d1a3fd7cf | ||
|
|
6625face50 | ||
|
|
d9764c251d | ||
|
|
243c8356e7 | ||
|
|
2d2f2ab02a | ||
|
|
50785e0577 | ||
|
|
9c1a827685 | ||
|
|
8c658b7eda | ||
|
|
26d8d3daee | ||
|
|
26476db95d | ||
|
|
3c90f6ae61 | ||
|
|
4889afdf0c | ||
|
|
2e777ea752 | ||
|
|
fddf1e21f3 | ||
|
|
d6013b10e6 | ||
|
|
aac6af40cc | ||
|
|
1f96a0e4d3 | ||
|
|
99716984ad | ||
|
|
24e9404a6f | ||
|
|
1f1adba5fd | ||
|
|
b907354746 | ||
|
|
9a08d4a412 | ||
|
|
3a08e6dd55 | ||
|
|
02f782a5ab | ||
|
|
30d68b2d1e | ||
|
|
8974cde582 | ||
|
|
5f3206a4e8 | ||
|
|
40e38d225e | ||
|
|
a73072c8fb | ||
|
|
6a5c5a4c08 | ||
|
|
fa7d5cee87 | ||
|
|
fe4db30e6d | ||
|
|
6936268fd2 | ||
|
|
e55f6ffdf3 | ||
|
|
e182e3c776 | ||
|
|
6309443d1f | ||
|
|
6f81bf00e8 | ||
|
|
86a1f5ee5d | ||
|
|
b3e460eebd | ||
|
|
5121c48c31 | ||
|
|
5f8e16cbe2 | ||
|
|
12e4f283fe | ||
|
|
20ca0896a6 |
+18
-3
@@ -1,15 +1,30 @@
|
||||
/logs/
|
||||
/target/
|
||||
/var/
|
||||
/frontend/node_modules/
|
||||
/frontend/public/
|
||||
/.vscode/
|
||||
/.idea/
|
||||
/bin/
|
||||
/.git/
|
||||
|
||||
# frontend files not needed in docker image
|
||||
/frontend/node_modules/
|
||||
/frontend/apps/remark42/node_modules/
|
||||
/frontend/apps/remark42/public/
|
||||
# e2e tests arficats
|
||||
/frontend/e2e/playwright-report/
|
||||
/frontend/e2e/playwright/.cache/
|
||||
/frontend/e2e/test-results/
|
||||
|
||||
# source files
|
||||
docker-compose.yml
|
||||
compose-dev-backend.yml
|
||||
compose-dev-frontend.yml
|
||||
compose-private-backend.yml
|
||||
compose-private-frontend.yml
|
||||
compose-e2e-test.yml
|
||||
compose-private.yml
|
||||
rest-client.env.json
|
||||
Makefile
|
||||
|
||||
# generated files
|
||||
*.cov
|
||||
@@ -21,4 +36,4 @@ debug.test
|
||||
*.test
|
||||
remark42
|
||||
/backend/var/
|
||||
compose-private-backend.yml
|
||||
/playwright-report/
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
|
||||
+2
-1
@@ -2,4 +2,5 @@
|
||||
# Unless a later match takes precedence, @umputun will be requested for
|
||||
# review when someone opens a pull request.
|
||||
|
||||
* @umputun
|
||||
* @umputun
|
||||
frontend/* @akellbl4 @Mavrin
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: test_backend
|
||||
name: backend
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -10,17 +10,19 @@ on:
|
||||
- "!backend/scripts/**"
|
||||
- "!**.md"
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
paths:
|
||||
- ".github/workflows/ci-test-backend.yml"
|
||||
- "backend/**"
|
||||
- "!backend/scripts/**"
|
||||
- "!**.md"
|
||||
jobs:
|
||||
test-backend:
|
||||
test:
|
||||
name: Test & Coverage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: debug if needed
|
||||
run: if [[ "$DEBUG" == "true" ]]; then env; fi
|
||||
@@ -28,37 +30,44 @@ jobs:
|
||||
DEBUG: ${{secrets.DEBUG}}
|
||||
|
||||
- name: install go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.17
|
||||
go-version: "1.20"
|
||||
|
||||
- name: install golangci-lint and goveralls
|
||||
run: |
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.44.0
|
||||
go get -u github.com/mattn/goveralls
|
||||
|
||||
- name: test and lint backend
|
||||
- name: test and build backend
|
||||
run: |
|
||||
go test -race -timeout=60s -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp ./...
|
||||
cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "_mock.go" > $GITHUB_WORKSPACE/profile.cov
|
||||
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run --out-format=github-actions ./...
|
||||
go build -race ./...
|
||||
working-directory: backend/app
|
||||
env:
|
||||
GOFLAGS: "-mod=vendor"
|
||||
TZ: "America/Chicago"
|
||||
|
||||
- name: test and lint examples
|
||||
- name: test examples
|
||||
run: |
|
||||
go version
|
||||
$GITHUB_WORKSPACE/golangci-lint version
|
||||
go test -race ./...
|
||||
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run --out-format=github-actions ./...
|
||||
go build -race ./...
|
||||
working-directory: backend/_example/memory_store
|
||||
env:
|
||||
TZ: "America/Chicago"
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
working-directory: backend/app
|
||||
|
||||
- name: golangci-lint on example directory
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: --config ../../.golangci.yml
|
||||
working-directory: backend/_example/memory_store
|
||||
|
||||
- name: submit coverage
|
||||
run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov
|
||||
run: |
|
||||
go install github.com/mattn/goveralls@latest
|
||||
goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov
|
||||
working-directory: backend
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -7,16 +7,17 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/ci-build.yml"
|
||||
- "backend/**"
|
||||
- "frontend/**"
|
||||
- "frontend/apps/**"
|
||||
- ".dockerignore"
|
||||
- "docker-init.sh"
|
||||
- "Dockerfile"
|
||||
- "!**.md"
|
||||
- "!frontend/packages/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/ci-build.yml"
|
||||
- "backend/**"
|
||||
- "frontend/**"
|
||||
- "frontend/apps/**"
|
||||
- ".dockerignore"
|
||||
- "docker-init.sh"
|
||||
- "Dockerfile"
|
||||
@@ -24,21 +25,36 @@ on:
|
||||
|
||||
jobs:
|
||||
build-images:
|
||||
name: Build Docker images
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: build docker image without pushing (only outside master)
|
||||
if: ${{ github.ref != 'refs/heads/master' }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true \
|
||||
--platform linux/amd64 .
|
||||
|
||||
- name: build example docker image without pushing (only outside master)
|
||||
if: ${{ github.ref != 'refs/heads/master' }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true \
|
||||
--platform linux/amd64 -f backend/_example/memory_store/Dockerfile .
|
||||
|
||||
- name: build and deploy master image to ghcr.io and dockerhub
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
env:
|
||||
@@ -51,12 +67,12 @@ jobs:
|
||||
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
|
||||
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
|
||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||
echo ${DOCKER_HUB_TOKEN} | docker login -u ${USERNAME} --password-stdin
|
||||
echo ${DOCKER_HUB_TOKEN} | docker login -u umputun --password-stdin
|
||||
docker buildx build --push \
|
||||
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
|
||||
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
|
||||
--platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||
-t ghcr.io/${USERNAME}/remark42:${ref} -t ${USERNAME}/remark42:${ref} .
|
||||
-t ghcr.io/umputun/remark42:${ref} -t umputun/remark42:${ref} .
|
||||
|
||||
- name: deploy tagged (latest) to ghcr.io and dockerhub
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
@@ -70,7 +86,7 @@ jobs:
|
||||
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
|
||||
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
|
||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||
echo ${DOCKER_HUB_TOKEN} | docker login -u ${USERNAME} --password-stdin
|
||||
echo ${DOCKER_HUB_TOKEN} | docker login -u umputun --password-stdin
|
||||
docker buildx build --push \
|
||||
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
|
||||
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
name: "@remark42/api"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- ".github/workflows/ci-frontend-api.yml"
|
||||
- "frontend/packages/**"
|
||||
- "!**.md"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/ci-frontend-api.yml"
|
||||
- "frontend/packages/**"
|
||||
- "!**.md"
|
||||
|
||||
jobs:
|
||||
type-check:
|
||||
name: Type check
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Run type check
|
||||
run: pnpm type-check:api
|
||||
working-directory: ./frontend
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Run linters
|
||||
run: pnpm lint:api
|
||||
working-directory: ./frontend/
|
||||
|
||||
test:
|
||||
name: Tests & Coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.15.1]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Test & Coverage
|
||||
run: pnpm coverage:api
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Submit coverage
|
||||
run: ${{ github.workspace }}/frontend/apps/remark42/node_modules/.bin/codecov
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -1,20 +0,0 @@
|
||||
name: frontend
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/ci-frontend-size-limit.yml"
|
||||
- "frontend/**"
|
||||
- "!**.md"
|
||||
|
||||
jobs:
|
||||
size:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI_JOB_NUMBER: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: andresz1/size-limit-action@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
directory: frontend
|
||||
@@ -3,105 +3,219 @@ name: frontend
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
tags:
|
||||
- master
|
||||
paths:
|
||||
- ".github/workflows/ci-frontend.yml"
|
||||
- "frontend/**"
|
||||
- "frontend/apps/remark42/**"
|
||||
- "!**.md"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/ci-frontend.yml"
|
||||
- "frontend/**"
|
||||
- "frontend/apps/remark42/**"
|
||||
- "!**.md"
|
||||
|
||||
jobs:
|
||||
check-translations:
|
||||
translations-check:
|
||||
name: Translations check
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.13.2]
|
||||
node: [16.15.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- run: npm ci --loglevel warn
|
||||
working-directory: ./frontend
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
path: ${{ github.workspace }}/frontend/node_modules/.cache
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- run: npm run check:translation
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./frontend
|
||||
|
||||
check-typescript:
|
||||
- name: Translations check
|
||||
run: pnpm translation-check
|
||||
working-directory: ./frontend/apps/remark42
|
||||
|
||||
type-check:
|
||||
name: Type check
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.13.2]
|
||||
node: [16.15.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- run: npm ci --loglevel warn
|
||||
working-directory: ./frontend
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
path: ${{ github.workspace }}/frontend/node_modules/.cache
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- run: npm run check:types
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Run type check
|
||||
run: pnpm type-check
|
||||
working-directory: ./frontend/apps/remark42
|
||||
|
||||
lint:
|
||||
name: Eslint & Stylelint
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.13.2]
|
||||
node: [16.15.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- run: npm ci --loglevel warn
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./frontend
|
||||
|
||||
- run: npm run lint
|
||||
working-directory: ./frontend
|
||||
- name: Run linters
|
||||
run: pnpm lint
|
||||
working-directory: ./frontend/apps/remark42
|
||||
|
||||
size-limit:
|
||||
name: Size limit
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
CI_JOB_NUMBER: 1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Check bundle size
|
||||
uses: andresz1/size-limit-action@dd31dce7dcc72a041fd3e49abf0502b13fc4ce05
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
directory: ./frontend/apps/remark42
|
||||
package_manager: pnpm
|
||||
|
||||
test:
|
||||
name: Tests & Coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16.13.2]
|
||||
node: [16.15.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- run: npm ci --loglevel warn
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
working-directory: ./frontend
|
||||
|
||||
- run: npm run test:coverage
|
||||
working-directory: ./frontend
|
||||
- name: Test & Coverage
|
||||
run: pnpm coverage
|
||||
working-directory: ./frontend/apps/remark42
|
||||
|
||||
- name: submit coverage
|
||||
run: node ${{ github.workspace }}/frontend/node_modules/.bin/codecov
|
||||
- name: Submit coverage
|
||||
run: ${{ github.workspace }}/frontend/apps/remark42/node_modules/.bin/codecov
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -3,6 +3,7 @@ name: site
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
paths:
|
||||
- ".github/workflows/ci-site.yml"
|
||||
@@ -13,20 +14,20 @@ on:
|
||||
- "site/**"
|
||||
|
||||
jobs:
|
||||
|
||||
build-site:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
@@ -44,7 +45,7 @@ jobs:
|
||||
echo GITHUB_REF - $ref
|
||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||
-t ghcr.io/${USERNAME}/remark24-site:${ref} .
|
||||
-t ghcr.io/umputun/remark42-site:${ref} .
|
||||
|
||||
- name: deploy tagged (latest) to ghcr.io and dockerhub
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
@@ -59,7 +60,7 @@ jobs:
|
||||
echo "GITHUB_REF=$ref, GITHUB_SHA=${GITHUB_SHA}"
|
||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||
-t ghcr.io/${USERNAME}/remark24-site:${ref} -t ghcr.io/${USERNAME}/remark24-site:latest .
|
||||
-t ghcr.io/umputun/remark42-site:${ref} -t ghcr.io/umputun/remark42-site:latest .
|
||||
|
||||
- name: remote site deployment from master
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: e2e
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "frontend/apps/remark42/**"
|
||||
- "frontend/e2e/**"
|
||||
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- "frontend/apps/remark42/**"
|
||||
- "frontend/e2e/**"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build & run containers
|
||||
id: tests
|
||||
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
+3
-2
@@ -4,7 +4,6 @@ target
|
||||
/logs/
|
||||
/target/
|
||||
/var/
|
||||
/web/
|
||||
debug
|
||||
debug.test
|
||||
.vscode
|
||||
@@ -18,10 +17,12 @@ remark42
|
||||
/bin/
|
||||
/backend/var/
|
||||
/backend/app/var/
|
||||
/backend/web/
|
||||
/backend/app/cmd/web/
|
||||
/backend/*.html.tmpl
|
||||
compose-private-backend.yml
|
||||
compose-private-frontend.yml
|
||||
compose-private.yml
|
||||
/backend/_example/*/vendor
|
||||
http-client.env.json
|
||||
/playwright-report/
|
||||
/backend/app/cmd/var
|
||||
|
||||
+66
-51
@@ -1,4 +1,51 @@
|
||||
FROM umputun/baseimage:buildgo-v1.8.0 as build-backend
|
||||
FROM --platform=$BUILDPLATFORM node:16.15.1-alpine AS frontend-deps
|
||||
|
||||
ARG SKIP_FRONTEND_TEST
|
||||
ARG SKIP_FRONTEND_BUILD
|
||||
|
||||
WORKDIR /srv/frontend/
|
||||
|
||||
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml ./frontend/pnpm-workspace.yaml /srv/frontend/
|
||||
COPY ./frontend/apps/remark42/package.json /srv/frontend/apps/remark42/
|
||||
|
||||
RUN \
|
||||
if [[ -z "$SKIP_FRONTEND_BUILD" || -z "$SKIP_FRONTEND_TEST" ]]; then \
|
||||
apk add --no-cache --update git && \
|
||||
npm i -g pnpm@7; \
|
||||
fi
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store/v3 \
|
||||
if [[ -z "$SKIP_FRONTEND_BUILD" || -z "$SKIP_FRONTEND_TEST" ]]; then \
|
||||
pnpm i; \
|
||||
fi
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM frontend-deps AS build-frontend
|
||||
|
||||
ARG SKIP_FRONTEND_TEST
|
||||
ARG SKIP_FRONTEND_BUILD
|
||||
ENV CI=true
|
||||
|
||||
WORKDIR /srv/frontend/apps/remark42/
|
||||
|
||||
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
|
||||
|
||||
RUN \
|
||||
if [ -z "$SKIP_FRONTEND_TEST" ]; then \
|
||||
pnpm lint type-check translation-check test; \
|
||||
else \
|
||||
echo 'Skip frontend test'; \
|
||||
fi
|
||||
|
||||
RUN \
|
||||
if [ -z "$SKIP_FRONTEND_BUILD" ]; then \
|
||||
pnpm build; \
|
||||
else \
|
||||
mkdir /srv/frontend/apps/remark42/public; \
|
||||
echo 'Skip frontend build'; \
|
||||
fi
|
||||
|
||||
FROM umputun/baseimage:buildgo-v1.11.0 as build-backend
|
||||
|
||||
ARG CI
|
||||
ARG GITHUB_REF
|
||||
@@ -8,11 +55,11 @@ ARG SKIP_BACKEND_TEST
|
||||
ARG BACKEND_TEST_TIMEOUT
|
||||
|
||||
ADD backend /build/backend
|
||||
ADD .git/ /build/backend/.git/
|
||||
# to embed the frontend files statically into Remark42 binary
|
||||
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /build/backend/app/cmd/web/
|
||||
RUN find /build/backend/app/cmd/web/ -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;
|
||||
WORKDIR /build/backend
|
||||
|
||||
ENV GOFLAGS="-mod=vendor"
|
||||
|
||||
# install gcc in order to be able to go test package with -race
|
||||
RUN apk --no-cache add gcc libc-dev
|
||||
|
||||
@@ -23,10 +70,10 @@ RUN \
|
||||
cd app && \
|
||||
if [ -z "$SKIP_BACKEND_TEST" ] ; then \
|
||||
CGO_ENABLED=1 go test -race -p 1 -timeout="${BACKEND_TEST_TIMEOUT:-300s}" -covermode=atomic -coverprofile=/profile.cov_tmp ./... && \
|
||||
cat /profile.cov_tmp | grep -v "_mock.go" > /profile.cov ; \
|
||||
cat /profile.cov_tmp | grep -v "_mock.go" > /profile.cov && \
|
||||
golangci-lint run --config ../.golangci.yml ./... ; \
|
||||
else \
|
||||
echo "skip backend tests and linter" \
|
||||
echo "skip backend tests and linter" \
|
||||
; fi
|
||||
|
||||
RUN \
|
||||
@@ -34,65 +81,33 @@ RUN \
|
||||
echo "version=$version" && \
|
||||
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:16.13.2-alpine as build-frontend-deps
|
||||
FROM umputun/baseimage:app-v1.11.0
|
||||
|
||||
ARG CI
|
||||
ARG SKIP_FRONTEND_BUILD
|
||||
ENV HUSKY_SKIP_INSTALL=true
|
||||
ARG GITHUB_SHA
|
||||
|
||||
RUN if [ -z "$SKIP_FRONTEND_BUILD" ] ; then \
|
||||
apk add --no-cache --update git \
|
||||
; fi
|
||||
ADD frontend/package.json /srv/frontend/package.json
|
||||
ADD frontend/package-lock.json /srv/frontend/package-lock.json
|
||||
WORKDIR /srv/frontend
|
||||
RUN mkdir node_modules
|
||||
RUN if [ -z "$SKIP_FRONTEND_BUILD" ] ; then \
|
||||
CI=true npm ci --loglevel warn \
|
||||
else \
|
||||
echo "skip frontend build" \
|
||||
; fi
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:16.13.2-alpine as build-frontend
|
||||
|
||||
ARG CI
|
||||
ARG SKIP_FRONTEND_TEST
|
||||
ARG SKIP_FRONTEND_BUILD
|
||||
ARG NODE_ENV=production
|
||||
|
||||
COPY --from=build-frontend-deps /srv/frontend/node_modules /srv/frontend/node_modules
|
||||
ADD frontend /srv/frontend
|
||||
WORKDIR /srv/frontend
|
||||
RUN mkdir public
|
||||
RUN if [ -z "$SKIP_FRONTEND_BUILD" ] ; then \
|
||||
if [ -z "$SKIP_FRONTEND_TEST" ] ; then \
|
||||
npm run lint test check; \
|
||||
else \
|
||||
echo "skip frontend tests and lint" ; npm run build \
|
||||
; fi \
|
||||
; fi
|
||||
RUN rm -rf ./node_modules
|
||||
|
||||
FROM umputun/baseimage:app-v1.8.0
|
||||
LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
|
||||
org.opencontainers.image.description="Remark42 comment engine" \
|
||||
org.opencontainers.image.documentation="https://remark42.com/docs/getting-started/" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
|
||||
org.opencontainers.image.title="Remark42" \
|
||||
org.opencontainers.image.url="https://remark42.com/" \
|
||||
org.opencontainers.image.revision="${GITHUB_SHA}"
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
ADD docker-init.sh /entrypoint.sh
|
||||
COPY docker-init.sh /srv/init.sh
|
||||
ADD backend/scripts/backup.sh /usr/local/bin/backup
|
||||
ADD backend/scripts/restore.sh /usr/local/bin/restore
|
||||
ADD backend/scripts/import.sh /usr/local/bin/import
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import
|
||||
RUN chmod +x /srv/init.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import
|
||||
|
||||
COPY --from=build-backend /build/backend/remark42 /srv/remark42
|
||||
COPY --from=build-backend /build/backend/templates /srv
|
||||
COPY --from=build-frontend /srv/frontend/public/ /srv/web
|
||||
COPY docker-init.sh /srv/init.sh
|
||||
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /srv/web/
|
||||
RUN chown -R app:app /srv
|
||||
RUN ln -s /srv/remark42 /usr/bin/remark42
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
|
||||
|
||||
|
||||
RUN chmod +x /srv/init.sh
|
||||
CMD ["/srv/remark42", "server"]
|
||||
|
||||
+20
-32
@@ -1,53 +1,41 @@
|
||||
FROM node:12.16-alpine as build-frontend-deps
|
||||
FROM node:16.15.1-alpine AS frontend-deps
|
||||
|
||||
ARG CI
|
||||
ENV CI=true
|
||||
|
||||
ENV SKIP_FRONTEND_TEST=true
|
||||
WORKDIR /srv/frontend
|
||||
|
||||
RUN apk add --no-cache --update git
|
||||
ADD frontend/package.json /srv/frontend/package.json
|
||||
ADD frontend/package-lock.json /srv/frontend/package-lock.json
|
||||
RUN cd /srv/frontend && CI=true npm ci
|
||||
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml ./frontend/pnpm-workspace.yaml /srv/frontend/
|
||||
COPY ./frontend/apps/remark42/package.json /srv/frontend/apps/remark42/package.json
|
||||
|
||||
FROM node:12.16-alpine as build-frontend
|
||||
RUN apk add --no-cache --update git && npm i -g pnpm@7
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store/v3 pnpm i
|
||||
|
||||
ARG CI
|
||||
ARG NODE_ENV=production
|
||||
ENV SKIP_FRONTEND_TEST=true
|
||||
ENV HUSKY_SKIP_INSTALL=true
|
||||
FROM frontend-deps AS build-frontend
|
||||
|
||||
COPY --from=build-frontend-deps /srv/frontend/node_modules /srv/frontend/node_modules
|
||||
ADD frontend /srv/frontend
|
||||
RUN cd /srv/frontend && \
|
||||
npm run build && \
|
||||
rm -rf ./node_modules
|
||||
ENV NODE_ENV=production
|
||||
ENV CI=true
|
||||
|
||||
FROM umputun/baseimage:buildgo-latest as build-backend
|
||||
WORKDIR /srv/frontend/apps/remark42/
|
||||
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
|
||||
RUN pnpm build
|
||||
|
||||
FROM umputun/baseimage:buildgo-v1.9.2 as build-backend
|
||||
|
||||
ARG GITHUB_TOKEN
|
||||
ENV SKIP_BACKEND_TEST=true
|
||||
ARG GITHUB_REF
|
||||
ARG GITHUB_SHA
|
||||
|
||||
WORKDIR /build/backend
|
||||
ADD backend /build/backend
|
||||
ADD README.md /build/
|
||||
ADD LICENSE /build/
|
||||
|
||||
ADD .git/ /build/backend/.git/
|
||||
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /build/backend/app/cmd/web/
|
||||
|
||||
COPY --from=build-frontend /srv/frontend/public/ web
|
||||
RUN find /build/backend/app/cmd/web/ -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;
|
||||
|
||||
RUN \
|
||||
export WEB_ROOT=/build/backend/web && \
|
||||
find . -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \; && \
|
||||
statik --src=${WEB_ROOT} --dest=/build/backend/app/rest -p api -f && \
|
||||
statik --src=/build/backend/templates --dest=/build/backend/app -p templates -ns templates -f && \
|
||||
ls -la /build/backend/app/templates/statik.go && \
|
||||
ls -la /build/backend/app/rest/api/statik.go && \
|
||||
ls -la /build/backend/web/
|
||||
|
||||
RUN \
|
||||
version=$("/script/version.sh") && echo "version=${version}" && \
|
||||
export GOFLAGS="-mod=vendor" && \
|
||||
version=$("/script/version.sh") && echo "version=${version}" && \
|
||||
GOOS=linux GOARCH=amd64 go build -o remark42.linux-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \
|
||||
GOOS=linux GOARCH=386 go build -o remark42.linux-386 -ldflags "-X main.revision=${version} -s -w" ./app && \
|
||||
GOOS=linux GOARCH=arm go build -o remark42.linux-arm -ldflags "-X main.revision=${version} -s -w" ./app && \
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
OS=linux
|
||||
ARCH=amd64
|
||||
GITHUB_REF=$(shell git rev-parse --symbolic-full-name HEAD)
|
||||
GITHUB_SHA=$(shell git rev-parse --short HEAD)
|
||||
|
||||
bin:
|
||||
docker build -f Dockerfile.artifacts -t remark42.bin .
|
||||
@@ -9,15 +11,18 @@ bin:
|
||||
docker rm -f remark42.bin
|
||||
|
||||
docker:
|
||||
docker build -t umputun/remark42 --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true .
|
||||
DOCKER_BUILDKIT=1 docker build -t umputun/remark42 --build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) \
|
||||
--build-arg CI=true --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true .
|
||||
|
||||
dockerx:
|
||||
docker buildx build --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true \
|
||||
--progress=plain --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||
-t ghcr.io/umputun/remark42:master -t umputun/remark42:master .
|
||||
docker buildx build --build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) --build-arg CI=true \
|
||||
--build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true \
|
||||
--progress=plain --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||
-t ghcr.io/umputun/remark42:master -t umputun/remark42:master .
|
||||
|
||||
release:
|
||||
docker build -f Dockerfile.artifacts --no-cache --pull -t remark42.bin .
|
||||
docker build -f Dockerfile.artifacts --no-cache --pull --build-arg CI=true \
|
||||
--build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) -t remark42.bin .
|
||||
- @docker rm -f remark42.bin 2>/dev/null || exit 0
|
||||
- @mkdir -p bin
|
||||
docker run -d --name=remark42.bin remark42.bin
|
||||
@@ -25,12 +30,13 @@ release:
|
||||
docker cp remark42.bin:/artifacts/remark42.linux-386.tar.gz bin/remark42.linux-386.tar.gz
|
||||
docker cp remark42.bin:/artifacts/remark42.linux-arm64.tar.gz bin/remark42.linux-arm64.tar.gz
|
||||
docker cp remark42.bin:/artifacts/remark42.darwin-amd64.tar.gz bin/remark42.darwin-amd64.tar.gz
|
||||
docker cp remark42.bin:/artifacts/remark42.darwin-arm64.tar.gz bin/remark42.darwin-arm64.tar.gz
|
||||
docker cp remark42.bin:/artifacts/remark42.freebsd-amd64.tar.gz bin/remark42.freebsd-amd64.tar.gz
|
||||
docker cp remark42.bin:/artifacts/remark42.windows-amd64.zip bin/remark42.windows-amd64.zip
|
||||
docker rm -f remark42.bin
|
||||
|
||||
race_test:
|
||||
cd backend/app && go test -race -mod=vendor -timeout=60s -count 1 ./...
|
||||
cd backend/app && go test -race -timeout=60s -count 1 ./...
|
||||
|
||||
backend:
|
||||
docker-compose -f compose-dev-backend.yml build
|
||||
@@ -39,8 +45,11 @@ frontend:
|
||||
docker-compose -f compose-dev-frontend.yml build
|
||||
|
||||
rundev:
|
||||
docker pull umputun/baseimage:buildgo-latest
|
||||
SKIP_BACKEND_TEST=true SKIP_FRONTEND_TEST=true docker-compose -f compose-private.yml build
|
||||
SKIP_BACKEND_TEST=true SKIP_FRONTEND_TEST=true GITHUB_REF=$(GITHUB_REF) GITHUB_SHA=$(GITHUB_SHA) CI=true \
|
||||
docker-compose -f compose-private.yml build
|
||||
docker-compose -f compose-private.yml up
|
||||
|
||||
e2e:
|
||||
docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
|
||||
|
||||
.PHONY: bin backend
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Remark42 [](https://github.com/umputun/remark42/actions) [](https://goreportcard.com/report/github.com/umputun/remark42) [](https://coveralls.io/github/umputun/remark42?branch=master) [](https://app.codecov.io/gh/umputun/remark42)
|
||||
# Remark42 [](https://github.com/umputun/remark42/actions) [](https://hub.docker.com/r/umputun/remark42) [](https://goreportcard.com/report/github.com/umputun/remark42) [](https://coveralls.io/github/umputun/remark42?branch=master) [](https://app.codecov.io/gh/umputun/remark42)
|
||||
|
||||
Remark42 is a self-hosted, lightweight and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles, or any other place where readers add comments.
|
||||
|
||||
@@ -37,6 +37,12 @@ For admin screenshots see [Admin UI documentation](https://remark42.com/docs/man
|
||||
|
||||
All remark42 documentation is available [by the link](https://remark42.com/docs/getting-started/installation/).
|
||||
|
||||
## Contribution
|
||||
|
||||
In order to start and work on the project locally in development mode check our contribution documentation for [backend](https://remark42.com/docs/contributing/backend/) and [frontend](https://remark42.com/docs/contributing/frontend/).
|
||||
|
||||
If you are interested in adding a new localization please check [these docs](https://remark42.com/docs/contributing/translations/).
|
||||
|
||||
## Related projects
|
||||
|
||||
* [A Helm chart for Remark42 on Kubernetes](https://github.com/groundhog2k/helm-charts/tree/master/charts/remark42)
|
||||
|
||||
@@ -8,8 +8,6 @@ run:
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
golint:
|
||||
min-confidence: 0.1
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
@@ -37,18 +35,14 @@ linters:
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
- structcheck
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- varcheck
|
||||
- deadcode
|
||||
- unused
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- varcheck
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- exportloopref
|
||||
@@ -64,9 +58,9 @@ issues:
|
||||
- text: "at least one file in a package should have a package comment"
|
||||
linters:
|
||||
- stylecheck
|
||||
- text: "should have a package comment, unless it's in another file for this package"
|
||||
- text: "package-comments: should have a package comment"
|
||||
linters:
|
||||
- golint
|
||||
- revive
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../site/src/docs/contributing/backend/index.md
|
||||
@@ -1,12 +1,22 @@
|
||||
FROM umputun/baseimage:buildgo-latest as build-backend
|
||||
FROM umputun/baseimage:buildgo-v1.9.2 as build-backend
|
||||
|
||||
ADD backend /build/backend
|
||||
WORKDIR /build/backend/_example/memory_store
|
||||
|
||||
RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
|
||||
|
||||
FROM umputun/baseimage:app-v1.9.2
|
||||
|
||||
FROM umputun/baseimage:app-latest
|
||||
ARG GITHUB_SHA
|
||||
|
||||
LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
|
||||
org.opencontainers.image.description="Remark42 comment engine example JRPC memory store" \
|
||||
org.opencontainers.image.documentation="https://github.com/umputun/remark42/tree/master/backend/_example/memory_store" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
|
||||
org.opencontainers.image.title="Remark42 JRPC example memory store" \
|
||||
org.opencontainers.image.url="https://remark42.com/" \
|
||||
org.opencontainers.image.revision="${GITHUB_SHA}"
|
||||
|
||||
WORKDIR /srv
|
||||
COPY --from=build-backend /build/bin/memory_store /srv/memory_store
|
||||
|
||||
@@ -35,7 +35,8 @@ func NewMemAdminStore(key string) *MemAdmin {
|
||||
return &MemAdmin{data: map[string]AdminRec{}, key: key}
|
||||
}
|
||||
|
||||
// Key executes find by siteID and returns substructure with secret key
|
||||
// Key supposed to execute find by siteID and returns substructure with secret key,
|
||||
// but in this case the shared secret is used for all sites
|
||||
func (m *MemAdmin) Key(_ string) (key string, err error) {
|
||||
return m.key, nil
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ package accessor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/umputun/remark42/backend/app/store"
|
||||
"github.com/umputun/remark42/backend/app/store/engine"
|
||||
)
|
||||
@@ -267,7 +267,7 @@ func (m *MemData) ListFlags(req engine.FlagRequest) (res []interface{}, err erro
|
||||
return res, nil
|
||||
|
||||
case engine.Blocked:
|
||||
log.Printf("%+v", m.metaUsers)
|
||||
log.Printf("[INFO] metaUsers: %+v", m.metaUsers)
|
||||
for _, u := range m.metaUsers {
|
||||
if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
|
||||
res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
|
||||
|
||||
@@ -624,6 +624,7 @@ func TestMemData_DeleteComment(t *testing.T) {
|
||||
func TestMemData_Close(t *testing.T) {
|
||||
b := prepMem(t)
|
||||
assert.NoError(t, b.Close())
|
||||
assert.NoError(t, b.Close(), "second call should not result in panic or errors")
|
||||
}
|
||||
|
||||
func TestMemData_DeleteHard(t *testing.T) {
|
||||
|
||||
@@ -1,44 +1,42 @@
|
||||
module github.com/umputun/remark42/memory_store
|
||||
|
||||
go 1.17
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/go-pkgz/jrpc v0.2.0
|
||||
github.com/go-pkgz/lgr v0.10.4
|
||||
github.com/go-pkgz/jrpc v0.3.0
|
||||
github.com/go-pkgz/lgr v0.11.0
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/umputun/remark42/backend v1.9.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/umputun/remark42/backend v1.1000.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Depado/bfchroma v1.3.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/Depado/bfchroma/v2 v2.0.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/didip/tollbooth/v6 v6.1.2 // indirect
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/go-chi/chi v4.1.1+incompatible // indirect
|
||||
github.com/go-chi/render v1.0.1 // indirect
|
||||
github.com/go-pkgz/expirable-cache v0.0.3 // indirect
|
||||
github.com/go-pkgz/rest v1.15.0 // indirect
|
||||
github.com/didip/tollbooth/v7 v7.0.1 // indirect
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||
github.com/go-pkgz/rest v1.17.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.18 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/umputun/remark42/backend => ../../
|
||||
replace github.com/umputun/remark42/backend v1.1000.0 => ../../
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
github.com/Depado/bfchroma v1.3.0 h1:zz14vpvySU6S0CL6yGPr1vkFevQecIt8dJdCsMS2JpM=
|
||||
github.com/Depado/bfchroma v1.3.0/go.mod h1:c0bFk0tFmT+clD3TIGurjWCfD/QV8/EebfM3JGr+98M=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
|
||||
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/didip/tollbooth/v6 v6.0.1/go.mod h1:j2pKs+JQ5PvU/K4jFnrnwntrmfUbYLJE5oSdxR37FD0=
|
||||
github.com/didip/tollbooth/v6 v6.1.2 h1:Kdqxmqw9YTv0uKajBUiWQg+GURL/k4vy9gmLCL01PjQ=
|
||||
github.com/didip/tollbooth/v6 v6.1.2/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20200524181329-8b84cd7183d9/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b h1:elkngQhLBcyDIXwL9Z7AfXXbBszUEsLxqNw6WPF8Mtc=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b/go.mod h1:0ZVa6kSzS011nfTC1rELyxK4tjVf6vqBnOv7oY2KlsA=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4=
|
||||
github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/didip/tollbooth/v7 v7.0.0/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||
github.com/didip/tollbooth/v7 v7.0.1 h1:TkT4sBKoQoHQFPf7blQ54iHrZiTDnr8TceU+MulVAog=
|
||||
github.com/didip/tollbooth/v7 v7.0.1/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f h1:jtKwihcLmUC9BAhoJ9adCUqdSSZcOdH2KL7mPTUm2aw=
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C80dnsuVRP2dAskjnXRNWdUJqtGgwG9wNrzt0019s=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc=
|
||||
github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ=
|
||||
github.com/go-pkgz/jrpc v0.2.0 h1:CLy/eZyekjraVrxZV18N2R1mYLMJ/nWrgdfyIOGPY/E=
|
||||
github.com/go-pkgz/jrpc v0.2.0/go.mod h1:wd8vtQ4CgtCnuqua6x2b1SKIgv0VSOh5Dn0uUITbiUE=
|
||||
github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw=
|
||||
github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0=
|
||||
github.com/go-pkgz/rest v1.5.0/go.mod h1:nQaM3RhSTUAmbBZWY4hfe4buyeC9VckvhoCktiQXJxI=
|
||||
github.com/go-pkgz/rest v1.15.0 h1:v/BDqJF9robo85GME85GWJ7O/NjtAO0x7LvO4EqWNRE=
|
||||
github.com/go-pkgz/rest v1.15.0/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 h1:ns5+1hjY8hntGv8bPaQd9Gr7Jyo+Uw5SLyII40aQdtA=
|
||||
github.com/go-pkgz/expirable-cache v1.0.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
|
||||
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
|
||||
github.com/go-pkgz/lgr v0.11.0 h1:9XH5o+vj09L0sRWEswIGK1lJ6g07xVB4/Z28RV9Z+qM=
|
||||
github.com/go-pkgz/lgr v0.11.0/go.mod h1:4rdRmMSs4yGFjnUg0rSDbKx21LmFNZoH4y8OLl3qDnU=
|
||||
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
||||
github.com/go-pkgz/rest v1.17.0 h1:LoBI/lDBMuqwWhOOkc6thM9NnwJO+K9nWvCOjZ7BAgE=
|
||||
github.com/go-pkgz/rest v1.17.0/go.mod h1:HHlLOt02NJc2sgffXBF6hYVMcRo4Gz3vjg43zTzN7VM=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -47,6 +46,7 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
@@ -54,65 +54,73 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
|
||||
github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -43,16 +43,14 @@ func main() {
|
||||
adminStore := accessor.NewMemAdminStore(opts.Secret)
|
||||
imgStore := accessor.NewMemImageStore()
|
||||
|
||||
rpcServer := jrpc.Server{
|
||||
API: opts.API,
|
||||
AuthUser: opts.AuthUser,
|
||||
AuthPasswd: opts.AuthPasswd,
|
||||
Version: revision,
|
||||
AppName: "remark42-memory",
|
||||
Logger: log.Default(),
|
||||
}
|
||||
rpcServer := jrpc.NewServer(
|
||||
opts.API,
|
||||
jrpc.Auth(opts.AuthUser, opts.AuthPasswd),
|
||||
jrpc.WithSignature("remark42-memory", "umputun", revision),
|
||||
jrpc.WithLogger(log.Default()),
|
||||
)
|
||||
|
||||
srv := server.NewRPC(dataStore, adminStore, imgStore, &rpcServer)
|
||||
srv := server.NewRPC(dataStore, adminStore, imgStore, rpcServer)
|
||||
|
||||
admRec := accessor.AdminRec{
|
||||
SiteID: "remark",
|
||||
|
||||
@@ -198,25 +198,62 @@ func TestRPC_listFlagsHndl(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "123456", id)
|
||||
|
||||
flagReq := engine.FlagRequest{
|
||||
// verify user
|
||||
verifyFlagReq := engine.FlagRequest{
|
||||
Flag: engine.Verified,
|
||||
UserID: "u1",
|
||||
Locator: store.Locator{
|
||||
SiteID: "test-site",
|
||||
},
|
||||
}
|
||||
flags, err := re.ListFlags(flagReq)
|
||||
flags, err := re.ListFlags(verifyFlagReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []interface{}{}, flags)
|
||||
assert.Empty(t, flags)
|
||||
|
||||
flagReq.Update = engine.FlagTrue
|
||||
status, err := re.Flag(flagReq)
|
||||
verifyFlagReq.Update = engine.FlagTrue
|
||||
status, err := re.Flag(verifyFlagReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, status)
|
||||
|
||||
flags, err = re.ListFlags(flagReq)
|
||||
flags, err = re.ListFlags(verifyFlagReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []interface{}{"u1"}, flags)
|
||||
verifiedUsers := make([]string, 0, len(flags))
|
||||
for _, v := range flags {
|
||||
verifiedUsers = append(verifiedUsers, v.(string))
|
||||
}
|
||||
assert.Equal(t, []string{"u1"}, verifiedUsers)
|
||||
|
||||
// block user
|
||||
blockFlagReq := engine.FlagRequest{
|
||||
Flag: engine.Blocked,
|
||||
UserID: "u1",
|
||||
Locator: store.Locator{
|
||||
SiteID: "test-site",
|
||||
},
|
||||
TTL: time.Hour,
|
||||
}
|
||||
flags, err = re.ListFlags(blockFlagReq)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, flags)
|
||||
|
||||
blockFlagReq.Update = engine.FlagTrue
|
||||
status, err = re.Flag(blockFlagReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, status)
|
||||
|
||||
flags, err = re.ListFlags(blockFlagReq)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, flags)
|
||||
blockedUsers := make([]store.BlockedUser, 0, len(flags))
|
||||
for _, v := range flags {
|
||||
blockedUsers = append(blockedUsers, v.(store.BlockedUser))
|
||||
}
|
||||
require.Equal(t, 1, len(blockedUsers))
|
||||
blockedUserInfo := blockedUsers[0]
|
||||
assert.Equal(t, "u1", blockedUserInfo.ID)
|
||||
assert.True(t, blockedUserInfo.Until.After(time.Now().Add(time.Minute*59)), "blocked duration is more than 59m away")
|
||||
assert.True(t, blockedUserInfo.Until.Before(time.Now().Add(time.Minute*61)), "blocked duration is less than 61m away")
|
||||
}
|
||||
|
||||
func TestRPC_userDetailHndl(t *testing.T) {
|
||||
@@ -301,6 +338,6 @@ func TestRPC_closeHndl(t *testing.T) {
|
||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||
|
||||
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||
err := re.Close()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, re.Close())
|
||||
assert.NoError(t, re.Close(), "second call should not result in panic or errors")
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func prepTestStore(t *testing.T) (port int, teardown func()) {
|
||||
mg := accessor.NewMemData()
|
||||
adm := accessor.NewMemAdminStore("secret")
|
||||
img := accessor.NewMemImageStore()
|
||||
s := NewRPC(mg, adm, img, &jrpc.Server{API: "/test", Logger: jrpc.NoOpLogger})
|
||||
s := NewRPC(mg, adm, img, jrpc.NewServer("/test"))
|
||||
|
||||
admRec := accessor.AdminRec{
|
||||
SiteID: "test-site",
|
||||
|
||||
@@ -14,11 +14,10 @@ import (
|
||||
// BackupCommand set of flags and command for export
|
||||
// ExportPath used as a separate element to leverage BACKUP_PATH. If ExportFile has a path (i.e. with /) BACKUP_PATH ignored.
|
||||
type BackupCommand struct {
|
||||
ExportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"`
|
||||
ExportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.TS}}.gz" description:"file name"`
|
||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||
Timeout time.Duration `long:"timeout" default:"60m" description:"export (backup) timeout"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
||||
ExportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"`
|
||||
ExportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.TS}}.gz" description:"file name"`
|
||||
|
||||
SupportCmdOpts
|
||||
CommonOpts
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
|
||||
// CleanupCommand set of flags and command for cleanup
|
||||
type CleanupCommand struct {
|
||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||
Dry bool `long:"dry" description:"dry mode, will not remove comments"`
|
||||
From string `long:"from" description:"from yyyymmdd"`
|
||||
To string `long:"to" description:"from yyyymmdd"`
|
||||
BadWords []string `short:"w" long:"bword" description:"bad word(s)"`
|
||||
BadUsers []string `short:"u" long:"buser" description:"bad user(s)"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
||||
SetTitle bool `long:"title" description:"title mode, will not remove comments, but reset titles to page's title'"`
|
||||
Dry bool `long:"dry" description:"dry mode, will not remove comments"`
|
||||
From string `long:"from" description:"from yyyymmdd"`
|
||||
To string `long:"to" description:"from yyyymmdd"`
|
||||
BadWords []string `short:"w" long:"bword" description:"bad word(s)"`
|
||||
BadUsers []string `short:"u" long:"buser" description:"bad user(s)"`
|
||||
SetTitle bool `long:"title" description:"title mode, will not remove comments, but reset titles to page's title'"`
|
||||
|
||||
SupportCmdOpts
|
||||
CommonOpts
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,14 @@ type CommonOpts struct {
|
||||
Revision string
|
||||
}
|
||||
|
||||
// SupportCmdOpts is set of commands shared among similar commands like backup/restore and such.
|
||||
// Order of fields defines the help command output order.
|
||||
type SupportCmdOpts struct {
|
||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"`
|
||||
Timeout time.Duration `long:"timeout" default:"60m" description:"timeout for the command run"`
|
||||
}
|
||||
|
||||
// DeprecatedFlag contains information about deprecated option
|
||||
type DeprecatedFlag struct {
|
||||
Old string
|
||||
|
||||
@@ -8,18 +8,16 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
)
|
||||
|
||||
// ImportCommand set of flags and command for import
|
||||
type ImportCommand struct {
|
||||
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
||||
Provider string `short:"p" long:"provider" default:"disqus" choice:"disqus" choice:"wordpress" choice:"commento" description:"import format"` //nolint
|
||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||
Timeout time.Duration `long:"timeout" default:"60m" description:"import timeout"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
||||
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
||||
Provider string `short:"p" long:"provider" default:"disqus" choice:"disqus" choice:"wordpress" choice:"commento" description:"import format"` //nolint
|
||||
|
||||
SupportCmdOpts
|
||||
CommonOpts
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
)
|
||||
@@ -14,10 +13,9 @@ import (
|
||||
// RemapCommand set of flags and command for change linkage between comments to
|
||||
// different urls based on given rules (input file)
|
||||
type RemapCommand struct {
|
||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
||||
Timeout time.Duration `long:"timeout" default:"60m" description:"remap timeout"`
|
||||
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
||||
|
||||
SupportCmdOpts
|
||||
CommonOpts
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ type RestoreCommand struct {
|
||||
ImportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"`
|
||||
ImportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.YYYYMMDD}}.gz" description:"file name" required:"true"`
|
||||
|
||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||
Timeout time.Duration `long:"timeout" default:"60m" description:"import timeout"`
|
||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
||||
SupportCmdOpts
|
||||
CommonOpts
|
||||
}
|
||||
|
||||
@@ -29,12 +27,10 @@ func (rc *RestoreCommand) Execute(args []string) error {
|
||||
return err
|
||||
}
|
||||
importer := ImportCommand{
|
||||
InputFile: fname,
|
||||
Site: rc.Site,
|
||||
Provider: "native",
|
||||
Timeout: rc.Timeout,
|
||||
AdminPasswd: rc.AdminPasswd,
|
||||
CommonOpts: rc.CommonOpts,
|
||||
InputFile: fname,
|
||||
Provider: "native",
|
||||
SupportCmdOpts: rc.SupportCmdOpts,
|
||||
CommonOpts: rc.CommonOpts,
|
||||
}
|
||||
return importer.Execute(args)
|
||||
}
|
||||
|
||||
+105
-81
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -41,6 +42,9 @@ import (
|
||||
"github.com/umputun/remark42/backend/app/templates"
|
||||
)
|
||||
|
||||
//go:embed web
|
||||
var webFS embed.FS
|
||||
|
||||
// ServerCommand with command line flags and env
|
||||
type ServerCommand struct {
|
||||
Store StoreGroup `group:"store" namespace:"store" env-namespace:"STORE"`
|
||||
@@ -79,7 +83,7 @@ type ServerCommand struct {
|
||||
EnableEmoji bool `long:"emoji" env:"EMOJI" description:"enable emoji"`
|
||||
SimpleView bool `long:"simple-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"`
|
||||
ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"`
|
||||
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments"`
|
||||
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments" env-delim:","`
|
||||
SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"`
|
||||
DisableSignature bool `long:"disable-signature" env:"DISABLE_SIGNATURE" description:"disable server signature in headers"`
|
||||
|
||||
@@ -92,16 +96,17 @@ type ServerCommand struct {
|
||||
SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"`
|
||||
SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint
|
||||
|
||||
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
|
||||
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
|
||||
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
|
||||
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
|
||||
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
|
||||
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
|
||||
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
|
||||
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
|
||||
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
|
||||
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
|
||||
Apple AppleGroup `group:"apple" namespace:"apple" env-namespace:"APPLE" description:"Apple OAuth"`
|
||||
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
|
||||
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
|
||||
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
|
||||
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
|
||||
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
|
||||
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
|
||||
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
|
||||
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
|
||||
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
|
||||
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
|
||||
Email struct {
|
||||
Enable bool `long:"enable" env:"ENABLE" description:"enable auth via email"`
|
||||
From string `long:"from" env:"FROM" description:"from email address"`
|
||||
@@ -129,6 +134,14 @@ type ImageProxyGroup struct {
|
||||
CacheExternal bool `long:"cache-external" env:"CACHE_EXTERNAL" description:"enable caching for external images"`
|
||||
}
|
||||
|
||||
// AppleGroup defines options for Apple auth params
|
||||
type AppleGroup struct {
|
||||
CID string `long:"cid" env:"CID" description:"Apple client ID"`
|
||||
TID string `long:"tid" env:"TID" description:"Apple service ID"`
|
||||
KID string `long:"kid" env:"KID" description:"Private key ID"`
|
||||
PrivateKeyFilePath string `long:"private-key-filepath" env:"PRIVATE_KEY_FILEPATH" description:"Private key file location" default:"/srv/var/apple.p8"`
|
||||
}
|
||||
|
||||
// AuthGroup defines options group for auth params
|
||||
type AuthGroup struct {
|
||||
CID string `long:"cid" env:"CID" description:"OAuth client ID"`
|
||||
@@ -193,7 +206,7 @@ type AdminGroup struct {
|
||||
Admins []string `long:"id" env:"ID" description:"admin(s) ids" env-delim:","`
|
||||
Email []string `long:"email" env:"EMAIL" description:"admin emails" env-delim:","`
|
||||
} `group:"shared" namespace:"shared" env-namespace:"SHARED"`
|
||||
RPC RPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"`
|
||||
RPC AdminRPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"`
|
||||
}
|
||||
|
||||
// TelegramGroup defines token for Telegram used in notify and auth modules
|
||||
@@ -204,13 +217,14 @@ type TelegramGroup struct {
|
||||
|
||||
// SMTPGroup defines options for SMTP server connection, used in auth and notify modules
|
||||
type SMTPGroup struct {
|
||||
Host string `long:"host" env:"HOST" description:"SMTP host"`
|
||||
Port int `long:"port" env:"PORT" description:"SMTP port"`
|
||||
Username string `long:"username" env:"USERNAME" description:"SMTP user name"`
|
||||
Password string `long:"password" env:"PASSWORD" description:"SMTP password"`
|
||||
TLS bool `long:"tls" env:"TLS" description:"enable TLS"`
|
||||
StartTLS bool `long:"starttls" env:"STARTTLS" description:"enable StartTLS"`
|
||||
TimeOut time.Duration `long:"timeout" env:"TIMEOUT" default:"10s" description:"SMTP TCP connection timeout"`
|
||||
Host string `long:"host" env:"HOST" description:"SMTP host"`
|
||||
Port int `long:"port" env:"PORT" description:"SMTP port"`
|
||||
Username string `long:"username" env:"USERNAME" description:"SMTP user name"`
|
||||
Password string `long:"password" env:"PASSWORD" description:"SMTP password"`
|
||||
TLS bool `long:"tls" env:"TLS" description:"enable TLS"`
|
||||
LoginAuth bool `long:"login_auth" env:"LOGIN_AUTH" description:"enable LOGIN auth instead of PLAIN"`
|
||||
StartTLS bool `long:"starttls" env:"STARTTLS" description:"enable StartTLS"`
|
||||
TimeOut time.Duration `long:"timeout" env:"TIMEOUT" default:"10s" description:"SMTP TCP connection timeout"`
|
||||
}
|
||||
|
||||
// NotifyGroup defines options for notification
|
||||
@@ -260,6 +274,12 @@ type RPCGroup struct {
|
||||
AuthPassword string `long:"auth_passwd" env:"AUTH_PASSWD" description:"basic auth user password"`
|
||||
}
|
||||
|
||||
// AdminRPCGroup defines options for remote admin store
|
||||
type AdminRPCGroup struct {
|
||||
RPCGroup
|
||||
SecretPerSite bool `long:"secret_per_site" env:"SECRET_PER_SITE" description:"enable JWT secret retrieval per aud, which is site_id in this case"`
|
||||
}
|
||||
|
||||
// LoadingCache defines interface for caching
|
||||
type LoadingCache interface {
|
||||
Get(key cache.Key, fn func() ([]byte, error)) (data []byte, err error) // load from cache if found or put to cache and return
|
||||
@@ -505,11 +525,12 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
||||
authenticator := s.getAuthenticator(dataService, avatarStore, adminStore, authRefreshCache)
|
||||
|
||||
telegramAuth := s.makeTelegramAuth(authenticator) // telegram auth requires TelegramAPI listener which is constructed below
|
||||
telegramService, telegramBotUsername := s.startTelegramAuthAndNotify(ctx, telegramAuth)
|
||||
telegramService := s.startTelegramAuthAndNotify(ctx, telegramAuth)
|
||||
|
||||
err = s.addAuthProviders(authenticator)
|
||||
if err != nil {
|
||||
_ = dataService.Close()
|
||||
_ = authRefreshCache.Close()
|
||||
return nil, fmt.Errorf("failed to make authenticator: %w", err)
|
||||
}
|
||||
|
||||
@@ -549,36 +570,38 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
||||
sslConfig, err := s.makeSSLConfig()
|
||||
if err != nil {
|
||||
_ = dataService.Close()
|
||||
_ = authRefreshCache.Close()
|
||||
return nil, fmt.Errorf("failed to make config of ssl server params: %w", err)
|
||||
}
|
||||
|
||||
srv := &api.Rest{
|
||||
Version: s.Revision,
|
||||
DataService: dataService,
|
||||
WebRoot: s.WebRoot,
|
||||
RemarkURL: s.RemarkURL,
|
||||
ImageProxy: imgProxy,
|
||||
CommentFormatter: commentFormatter,
|
||||
Migrator: migr,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
SharedSecret: s.SharedSecret,
|
||||
Authenticator: authenticator,
|
||||
Cache: loadingCache,
|
||||
NotifyService: notifyService,
|
||||
TelegramService: telegramService,
|
||||
SSLConfig: sslConfig,
|
||||
UpdateLimiter: s.UpdateLimit,
|
||||
ImageService: imageService,
|
||||
EmailNotifications: contains("email", s.Notify.Users),
|
||||
TelegramBotUsername: telegramBotUsername,
|
||||
EmojiEnabled: s.EnableEmoji,
|
||||
AnonVote: s.AnonymousVote && s.RestrictVoteIP,
|
||||
SimpleView: s.SimpleView,
|
||||
ProxyCORS: s.ProxyCORS,
|
||||
AllowedAncestors: s.AllowedHosts,
|
||||
SendJWTHeader: s.Auth.SendJWTHeader,
|
||||
SubscribersOnly: s.SubscribersOnly,
|
||||
DisableSignature: s.DisableSignature,
|
||||
Version: s.Revision,
|
||||
DataService: dataService,
|
||||
WebRoot: s.WebRoot,
|
||||
WebFS: webFS,
|
||||
RemarkURL: s.RemarkURL,
|
||||
ImageProxy: imgProxy,
|
||||
CommentFormatter: commentFormatter,
|
||||
Migrator: migr,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
SharedSecret: s.SharedSecret,
|
||||
Authenticator: authenticator,
|
||||
Cache: loadingCache,
|
||||
NotifyService: notifyService,
|
||||
TelegramService: telegramService,
|
||||
SSLConfig: sslConfig,
|
||||
UpdateLimiter: s.UpdateLimit,
|
||||
ImageService: imageService,
|
||||
EmailNotifications: contains("email", s.Notify.Users),
|
||||
TelegramNotifications: contains("telegram", s.Notify.Users) && telegramService != nil,
|
||||
EmojiEnabled: s.EnableEmoji,
|
||||
AnonVote: s.AnonymousVote && s.RestrictVoteIP,
|
||||
SimpleView: s.SimpleView,
|
||||
ProxyCORS: s.ProxyCORS,
|
||||
AllowedAncestors: s.AllowedHosts,
|
||||
SendJWTHeader: s.Auth.SendJWTHeader,
|
||||
SubscribersOnly: s.SubscribersOnly,
|
||||
DisableSignature: s.DisableSignature,
|
||||
}
|
||||
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||
@@ -588,6 +611,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
||||
da, errDevAuth := authenticator.DevAuth()
|
||||
if errDevAuth != nil {
|
||||
_ = dataService.Close()
|
||||
_ = authRefreshCache.Close()
|
||||
return nil, fmt.Errorf("can't make dev oauth2 server: %w", errDevAuth)
|
||||
}
|
||||
devAuth = da
|
||||
@@ -823,12 +847,28 @@ func (s *ServerCommand) makeCache() (LoadingCache, error) {
|
||||
return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // simple code but many if checks
|
||||
func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
||||
providersCount := 0
|
||||
if s.Auth.Telegram {
|
||||
providersCount++
|
||||
}
|
||||
|
||||
if s.Auth.Apple.CID != "" && s.Auth.Apple.TID != "" && s.Auth.Apple.KID != "" {
|
||||
err := authenticator.AddAppleProvider(
|
||||
provider.AppleConfig{
|
||||
ClientID: s.Auth.Apple.CID,
|
||||
TeamID: s.Auth.Apple.TID,
|
||||
KeyID: s.Auth.Apple.KID,
|
||||
ResponseMode: "query", // default is form_post which wouldn't work here
|
||||
},
|
||||
provider.LoadApplePrivateKeyFromFile(s.Auth.Apple.PrivateKeyFilePath),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providersCount++
|
||||
}
|
||||
if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" {
|
||||
authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC)
|
||||
providersCount++
|
||||
@@ -860,7 +900,11 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
||||
|
||||
if s.Auth.Dev {
|
||||
log.Print("[INFO] dev access enabled")
|
||||
authenticator.AddProvider("dev", "", "")
|
||||
u, errURL := url.Parse(s.RemarkURL)
|
||||
if errURL != nil {
|
||||
return fmt.Errorf("can't parse Remark42 URL: %w", errURL)
|
||||
}
|
||||
authenticator.AddDevProvider(u.Hostname(), 8084)
|
||||
providersCount++
|
||||
}
|
||||
|
||||
@@ -871,17 +915,20 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
||||
SMTPUserName: s.SMTP.Username,
|
||||
SMTPPassword: s.SMTP.Password,
|
||||
TimeOut: s.SMTP.TimeOut,
|
||||
StartTLS: s.SMTP.StartTLS,
|
||||
LoginAuth: s.SMTP.LoginAuth,
|
||||
TLS: s.SMTP.TLS,
|
||||
Charset: "UTF-8",
|
||||
From: s.Auth.Email.From,
|
||||
Subject: s.Auth.Email.Subject,
|
||||
ContentType: s.Auth.Email.ContentType,
|
||||
}
|
||||
sndr := sender.NewEmailClient(params, log.Default())
|
||||
tmpl, err := s.loadEmailTemplate()
|
||||
tmpl, err := templates.Read(s.Auth.Email.MsgTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authenticator.AddVerifProvider("email", tmpl, sndr)
|
||||
authenticator.AddVerifProvider("email", string(tmpl), sndr)
|
||||
}
|
||||
|
||||
if s.Auth.Anonymous {
|
||||
@@ -925,27 +972,6 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadEmailTemplate trying to get template from statik
|
||||
func (s *ServerCommand) loadEmailTemplate() (string, error) {
|
||||
var file []byte
|
||||
var err error
|
||||
|
||||
if s.Auth.Email.MsgTemplate == "email_confirmation_login.html.tmpl" {
|
||||
fs := templates.NewFS()
|
||||
file, err = fs.ReadFile(s.Auth.Email.MsgTemplate)
|
||||
} else {
|
||||
// deprecated loading from an external file, should be removed before v1.9.0
|
||||
file, err = os.ReadFile(s.Auth.Email.MsgTemplate)
|
||||
log.Printf("[INFO] template %s will be read from disk", s.Auth.Email.MsgTemplate)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read file %s: %w", s.Auth.Email.MsgTemplate, err)
|
||||
}
|
||||
|
||||
return string(file), nil
|
||||
}
|
||||
|
||||
// creates and registers telegram auth, which we need separately from other auth providers
|
||||
func (s *ServerCommand) makeTelegramAuth(authenticator *auth.Service) providers.TGUpdatesReceiver {
|
||||
if s.Auth.Telegram {
|
||||
@@ -967,7 +993,8 @@ func (s *ServerCommand) makeNotifyService(dataStore *service.DataStore, destinat
|
||||
if destinations == nil {
|
||||
destinations = []notify.Destination{}
|
||||
}
|
||||
if telegram != nil {
|
||||
// it's possible that telegram notification service was created for auth but should not be used for notifications
|
||||
if telegram != nil && (contains("telegram", s.Notify.Users) || contains("telegram", s.Notify.Admins)) {
|
||||
destinations = append(destinations, telegram)
|
||||
}
|
||||
|
||||
@@ -1042,6 +1069,7 @@ func (s *ServerCommand) makeNotifyDestinations(authenticator *auth.Service) ([]n
|
||||
Port: s.SMTP.Port,
|
||||
TLS: s.SMTP.TLS,
|
||||
StartTLS: s.SMTP.StartTLS,
|
||||
LoginAuth: s.SMTP.LoginAuth,
|
||||
Username: s.SMTP.Username,
|
||||
Password: s.SMTP.Password,
|
||||
TimeOut: s.SMTP.TimeOut,
|
||||
@@ -1118,7 +1146,7 @@ func (s *ServerCommand) getAuthenticator(ds *service.DataStore, avas avatar.Stor
|
||||
SameSiteCookie: s.parseSameSite(s.Auth.SameSite),
|
||||
SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"),
|
||||
SecretReader: token.SecretFunc(func(aud string) (string, error) { // get secret per site
|
||||
return admns.Key("")
|
||||
return admns.Key(aud)
|
||||
}),
|
||||
ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { // set attributes, on new token or refresh
|
||||
if c.User == nil {
|
||||
@@ -1163,6 +1191,7 @@ func (s *ServerCommand) getAuthenticator(ds *service.DataStore, avas avatar.Stor
|
||||
Logger: log.Default(),
|
||||
RefreshCache: authRefreshCache,
|
||||
UseGravatar: true,
|
||||
AudSecrets: s.Admin.RPC.SecretPerSite,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1183,20 +1212,15 @@ func (s *ServerCommand) parseSameSite(ss string) http.SameSite {
|
||||
|
||||
// startTelegramAuthAndNotify initializes telegram notify and auth Telegram Bot listen loop.
|
||||
// Does nothing if telegram auth and notifications are disabled.
|
||||
// Doesn't return telegram bot username if user notifications are disabled, as that is the way frontend knows they are enabled.
|
||||
func (s *ServerCommand) startTelegramAuthAndNotify(ctx context.Context, telegramAuth providers.TGUpdatesReceiver) (tg *notify.Telegram, telegramBotUsername string) {
|
||||
func (s *ServerCommand) startTelegramAuthAndNotify(ctx context.Context, telegramAuth providers.TGUpdatesReceiver) (tg *notify.Telegram) {
|
||||
if !contains("telegram", s.Notify.Users) && !contains("telegram", s.Notify.Admins) && !s.Auth.Telegram {
|
||||
return nil, ""
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if tg, err = s.makeTelegramNotify(); err != nil {
|
||||
log.Printf("[WARN] failed to make telegram notify service, %s", err)
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
if contains("telegram", s.Notify.Users) {
|
||||
telegramBotUsername = tg.GetBotUsername()
|
||||
return nil
|
||||
}
|
||||
|
||||
telegramReceivers := []providers.TGUpdatesReceiver{tg}
|
||||
@@ -1206,7 +1230,7 @@ func (s *ServerCommand) startTelegramAuthAndNotify(ctx context.Context, telegram
|
||||
// start bot messages receiver for both notify and auth services
|
||||
go providers.DispatchTelegramUpdates(ctx, tg, telegramReceivers, time.Second*5)
|
||||
|
||||
return tg, telegramBotUsername
|
||||
return tg
|
||||
}
|
||||
|
||||
// splitAtCommas split s at commas, ignoring commas in strings.
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestServerApp_DevMode(t *testing.T) {
|
||||
waitForHTTPServerStart(port)
|
||||
|
||||
providers := app.restSrv.Authenticator.Providers()
|
||||
require.Equal(t, 9+1, len(providers), "extra auth provider")
|
||||
require.Equal(t, 10+1, len(providers), "extra auth provider")
|
||||
assert.Equal(t, "dev", providers[len(providers)-2].Name(), "dev auth provider")
|
||||
// send ping
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/ping", port))
|
||||
@@ -107,7 +107,7 @@ func TestServerApp_AnonMode(t *testing.T) {
|
||||
waitForHTTPServerStart(port)
|
||||
|
||||
providers := app.restSrv.Authenticator.Providers()
|
||||
require.Equal(t, 9+1, len(providers), "extra auth provider for anon")
|
||||
require.Equal(t, 10+1, len(providers), "extra auth provider for anon")
|
||||
assert.Equal(t, "anonymous", providers[len(providers)-1].Name(), "anon auth provider")
|
||||
|
||||
client := http.Client{Timeout: 10 * time.Second}
|
||||
@@ -143,36 +143,42 @@ func TestServerApp_AnonMode(t *testing.T) {
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
// try to login with non-latin name
|
||||
time.Sleep(time.Second)
|
||||
resp, err = client.Get(fmt.Sprintf("http://localhost:%d/auth/anonymous/login?user=Раз_Два%20%20Три_34567&aud=remark", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// try to login with bad name
|
||||
time.Sleep(time.Second)
|
||||
resp, err = client.Get(fmt.Sprintf("http://localhost:%d/auth/anonymous/login?user=**blah123&aud=remark", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
// try to login with short name
|
||||
time.Sleep(time.Second)
|
||||
resp, err = client.Get(fmt.Sprintf("http://localhost:%d/auth/anonymous/login?user=bl%%20%%20&aud=remark", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
// try to login with name what have space in prefix
|
||||
time.Sleep(time.Second)
|
||||
resp, err = client.Get(fmt.Sprintf("http://localhost:%d/auth/anonymous/login?user=%%20somebody&aud=remark", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
// try to login with name what have space in suffix
|
||||
time.Sleep(time.Second)
|
||||
resp, err = client.Get(fmt.Sprintf("http://localhost:%d/auth/anonymous/login?user=somebody%%20&aud=remark", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
// try to login with long name
|
||||
time.Sleep(time.Second)
|
||||
ln := strings.Repeat("x", 65)
|
||||
resp, err = client.Get(fmt.Sprintf("http://localhost:%d/auth/anonymous/login?user=%s&aud=remark", port, ln))
|
||||
require.NoError(t, err)
|
||||
@@ -180,12 +186,14 @@ func TestServerApp_AnonMode(t *testing.T) {
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
// try to login with admin name
|
||||
time.Sleep(time.Second)
|
||||
resp, err = client.Get(fmt.Sprintf("http://localhost:%d/auth/anonymous/login?user=umpUtun&aud=remark", port))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// try to add a comment as anonymous with admin name
|
||||
time.Sleep(time.Second)
|
||||
req, err = http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/api/v1/comment", port),
|
||||
strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`))
|
||||
require.NoError(t, err)
|
||||
@@ -282,7 +290,8 @@ func TestServerApp_WithRemote(t *testing.T) {
|
||||
port := chooseRandomUnusedPort()
|
||||
_, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none",
|
||||
"--store.type=rpc", "--store.rpc.api=http://127.0.0.1",
|
||||
"--port=" + strconv.Itoa(port), "--admin.type=rpc", "--admin.rpc.api=http://127.0.0.1", "--avatar.fs.path=/tmp"})
|
||||
"--port=" + strconv.Itoa(port), "--avatar.fs.path=/tmp",
|
||||
"--admin.type=rpc", "--admin.rpc.secret_per_site", "--admin.rpc.api=http://127.0.0.1"})
|
||||
require.NoError(t, err)
|
||||
opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid"
|
||||
opts.BackupLocation, opts.Image.FS.Path = "/tmp", "/tmp"
|
||||
@@ -343,6 +352,7 @@ func TestServerApp_Failed(t *testing.T) {
|
||||
assert.EqualError(t, err, "invalid remark42 url demo.remark42.com")
|
||||
t.Log(err)
|
||||
|
||||
// wrong store type
|
||||
opts = ServerCommand{}
|
||||
opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"})
|
||||
|
||||
@@ -366,6 +376,19 @@ func TestServerApp_Failed(t *testing.T) {
|
||||
"problem subscribing to channel remark42-cache on address wrong_address: "+
|
||||
"dial tcp: address wrong_address: missing port in address")
|
||||
t.Log(err)
|
||||
|
||||
// wrong apple private key type
|
||||
opts = ServerCommand{}
|
||||
opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"})
|
||||
p = flags.NewParser(&opts, flags.Default)
|
||||
_, err = p.ParseArgs([]string{"--auth.apple.cid=123", "--auth.apple.tid=123",
|
||||
"--auth.apple.kid=123", "--auth.apple.private-key-filepath=testdata/apple-bad.p8"})
|
||||
assert.NoError(t, err)
|
||||
_, err = opts.newServerApp(context.Background())
|
||||
assert.EqualError(t, err,
|
||||
"failed to make authenticator: an AppleProvider creating failed: "+
|
||||
"provided private key is not ECDSA")
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func TestServerApp_Shutdown(t *testing.T) {
|
||||
@@ -589,7 +612,7 @@ func TestServerAuthHooks(t *testing.T) {
|
||||
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
},
|
||||
User: &token.User{
|
||||
ID: "dev",
|
||||
ID: "github_dev",
|
||||
Name: "developer one",
|
||||
},
|
||||
}
|
||||
@@ -628,14 +651,14 @@ func TestServerAuthHooks(t *testing.T) {
|
||||
require.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "user without aud claim rejected, \n"+tkNoAud+"\n"+string(body))
|
||||
|
||||
// block user dev as admin
|
||||
// block user github_dev as admin
|
||||
req, err = http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("http://localhost:%d/api/v1/admin/user/dev?site=remark&block=1&ttl=10d", port), http.NoBody)
|
||||
fmt.Sprintf("http://localhost:%d/api/v1/admin/user/github_dev?site=remark&block=1&ttl=10d", port), http.NoBody)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth("admin", "password")
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "user dev blocked")
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "user github_dev blocked")
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
@@ -659,19 +682,6 @@ func TestServerAuthHooks(t *testing.T) {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func TestServer_loadEmailTemplate(t *testing.T) {
|
||||
cmd := ServerCommand{}
|
||||
cmd.Auth.Email.MsgTemplate = "testdata/email.tmpl"
|
||||
r, err := cmd.loadEmailTemplate()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "The token is {{.Token}}", r)
|
||||
|
||||
cmd.Auth.Email.MsgTemplate = "badpath.tmpl"
|
||||
r, err = cmd.loadEmailTemplate()
|
||||
assert.EqualError(t, err, "failed to read file badpath.tmpl: open badpath.tmpl: no such file or directory")
|
||||
assert.Equal(t, r, "")
|
||||
}
|
||||
|
||||
func TestServerCommand_parseSameSite(t *testing.T) {
|
||||
tbl := []struct {
|
||||
inp string
|
||||
@@ -763,6 +773,8 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve
|
||||
cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation, cmd.Image.FS.Path = "/tmp/remark42_test", "fs", "/tmp/remark42_test", "/tmp/remark42_test"
|
||||
cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port)
|
||||
cmd.Store.Bolt.Timeout = 10 * time.Second
|
||||
cmd.Auth.Apple.CID, cmd.Auth.Apple.KID, cmd.Auth.Apple.TID = "cid", "kid", "tid"
|
||||
cmd.Auth.Apple.PrivateKeyFilePath = "testdata/apple.p8"
|
||||
cmd.Auth.Github.CSEC, cmd.Auth.Github.CID = "csec", "cid"
|
||||
cmd.Auth.Google.CSEC, cmd.Auth.Google.CID = "csec", "cid"
|
||||
cmd.Auth.Facebook.CSEC, cmd.Auth.Facebook.CID = "csec", "cid"
|
||||
@@ -809,8 +821,6 @@ func createAppFromCmd(t *testing.T, cmd ServerCommand) (*serverApp, context.Cont
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
app, err := cmd.newServerApp(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return app, ctx, cancel
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKNwapOQ6rQJHetP
|
||||
HRlJBIh1OsOsUBiXb3rXXE3xpWAxAha0MH+UPRblOko+5T2JqIb+xKf9Vi3oTM3t
|
||||
KvffaOPtzKXZauscjq6NGzA3LgeiMy6q19pvkUUOlGYK6+Xfl+B7Xw6+hBMkQuGE
|
||||
nUS8nkpR5mK4ne7djIyfHFfMu4ptAgMBAAECgYA+s0PPtMq1osG9oi4xoxeAGikf
|
||||
JB3eMUptP+2DYW7mRibc+ueYKhB9lhcUoKhlQUhL8bUUFVZYakP8xD21thmQqnC4
|
||||
f63asad0ycteJMLb3r+z26LHuCyOdPg1pyLk3oQ32lVQHBCYathRMcVznxOG16VK
|
||||
I8BFfstJTaJu0lK/wQJBANYFGusBiZsJQ3utrQMVPpKmloO2++4q1v6ZR4puDQHx
|
||||
TjLjAIgrkYfwTJBLBRZxec0E7TmuVQ9uJ+wMu/+7zaUCQQDDf2xMnQqYknJoKGq+
|
||||
oAnyC66UqWC5xAnQS32mlnJ632JXA0pf9pb1SXAYExB1p9Dfqd3VAwQDwBsDDgP6
|
||||
HD8pAkEA0lscNQZC2TaGtKZk2hXkdcH1SKru/g3vWTkRHxfCAznJUaza1fx0wzdG
|
||||
GcES1Bdez0tbW4llI5By/skZc2eE3QJAFl6fOskBbGHde3Oce0F+wdZ6XIJhEgCP
|
||||
iukIcKZoZQzoiMJUoVRrA5gqnmaYDI5uRRl/y57zt6YksR3KcLUIuQJAd242M/WF
|
||||
6YAZat3q/wEeETeQq1wrooew+8lHl05/Nt0cCpV48RGEhJ83pzBm3mnwHf8lTBJH
|
||||
x6XroMXsmbnsEw==
|
||||
-----END PRIVATE KEY-----
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgGH2MylyZjjRdauTk
|
||||
xxXW6p8VSHqIeVRRKSJPg1xn6+KgCgYIKoZIzj0DAQehRANCAAS/mNzQ7aBbIBr3
|
||||
DiHiJGIDEzi6+q3mmyhH6ZWQWFdFei2qgdyM1V6qtRPVq+yHBNSBebbR4noE/IYO
|
||||
hMdWYrKn
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
This stub page would be replaced by the frontend statically built HTML during the Docker image build.
|
||||
+3
-3
@@ -23,7 +23,8 @@ type Opts struct {
|
||||
CleanupCmd cmd.CleanupCommand `command:"cleanup"`
|
||||
RemapCmd cmd.RemapCommand `command:"remap"`
|
||||
|
||||
RemarkURL string `long:"url" env:"REMARK_URL" required:"true" description:"url to remark"`
|
||||
RemarkURL string `long:"url" env:"REMARK_URL" required:"true" description:"url to remark"`
|
||||
// SharedSecret is only used in server command, but defined for all commands for historical reasons
|
||||
SharedSecret string `long:"secret" env:"SECRET" required:"true" description:"the shared secret key used to sign JWT, should be a random, long, hard-to-guess string"`
|
||||
|
||||
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
||||
@@ -56,9 +57,8 @@ func main() {
|
||||
if _, err := p.Parse(); err != nil {
|
||||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -22,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func Test_Main(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "remark42")
|
||||
dir, err := os.MkdirTemp(os.TempDir(), "remark42")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
@@ -60,7 +59,7 @@ func Test_Main(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMain_WithWebhook(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "remark42")
|
||||
dir, err := os.MkdirTemp(os.TempDir(), "remark42")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ func (ab AutoBackup) removeOldBackupFiles() {
|
||||
backFiles = append(backFiles, info)
|
||||
}
|
||||
}
|
||||
sort.Slice(backFiles, func(i int, j int) bool { return backFiles[i].Name() < backFiles[j].Name() })
|
||||
sort.Slice(backFiles, func(i, j int) bool { return backFiles[i].Name() < backFiles[j].Name() })
|
||||
|
||||
if len(backFiles) > ab.KeepMax {
|
||||
for i := 0; i < len(backFiles)-ab.KeepMax; i++ {
|
||||
|
||||
@@ -38,7 +38,7 @@ type MapperMaker func(reader io.Reader) (Mapper, error)
|
||||
type Store interface {
|
||||
Create(comment store.Comment) (commentID string, err error)
|
||||
Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error)
|
||||
List(siteID string, limit int, skip int) ([]store.PostInfo, error)
|
||||
List(siteID string, limit, skip int) ([]store.PostInfo, error)
|
||||
DeleteAll(siteID string) error
|
||||
Metas(siteID string) (umetas []service.UserMetaData, pmetas []service.PostMetaData, err error)
|
||||
SetMetas(siteID string, umetas []service.UserMetaData, pmetas []service.PostMetaData) error
|
||||
|
||||
@@ -163,7 +163,7 @@ func TestNative_ImportManyWithError(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.WriteString(`{"version":1, "users":[], "posts":[]}` + "\n")
|
||||
for i := 0; i < 100; i++ {
|
||||
buf.WriteString(fmt.Sprintf(goodRec, i))
|
||||
fmt.Fprintf(buf, goodRec, i)
|
||||
}
|
||||
buf.WriteString("{}\n")
|
||||
buf.WriteString("{}\n")
|
||||
|
||||
@@ -100,7 +100,6 @@ func NewEmail(emailParams EmailParams, smtpParams ntf.SMTPParams) (*Email, error
|
||||
func (e *Email) setTemplates() error {
|
||||
var err error
|
||||
var msgTmplFile, verifyTmplFile []byte
|
||||
fs := templates.NewFS()
|
||||
|
||||
if e.VerificationTemplatePath == "" {
|
||||
e.VerificationTemplatePath = defaultEmailVerificationTemplatePath
|
||||
@@ -110,10 +109,10 @@ func (e *Email) setTemplates() error {
|
||||
e.MsgTemplatePath = defaultEmailTemplatePath
|
||||
}
|
||||
|
||||
if msgTmplFile, err = fs.ReadFile(e.MsgTemplatePath); err != nil {
|
||||
if msgTmplFile, err = templates.Read(e.MsgTemplatePath); err != nil {
|
||||
return fmt.Errorf("can't read message template: %w", err)
|
||||
}
|
||||
if verifyTmplFile, err = fs.ReadFile(e.VerificationTemplatePath); err != nil {
|
||||
if verifyTmplFile, err = templates.Read(e.VerificationTemplatePath); err != nil {
|
||||
return fmt.Errorf("can't read verification template: %w", err)
|
||||
}
|
||||
if e.msgTmpl, err = template.New("msgTmpl").Parse(string(msgTmplFile)); err != nil {
|
||||
|
||||
@@ -55,32 +55,18 @@ func Test_initTemplatesErr(t *testing.T) {
|
||||
errText string
|
||||
emailParams EmailParams
|
||||
}{
|
||||
{
|
||||
name: "with wrong (default, working in prod) path to reply template",
|
||||
errText: "can't read message template: open email_reply.html.tmpl: no such file or directory",
|
||||
emailParams: EmailParams{},
|
||||
},
|
||||
{
|
||||
name: "with wrong (default, working in prod) path to verification template",
|
||||
errText: "can't read verification template: open email_confirmation_subscription.html.tmpl: no such file or directory",
|
||||
emailParams: EmailParams{
|
||||
MsgTemplatePath: "testdata/msg.html.tmpl",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with wrong path to verification template",
|
||||
errText: "can't read verification template: open notfound.tmpl: no such file or directory",
|
||||
errText: "notfound.tmpl: file does not exist",
|
||||
emailParams: EmailParams{
|
||||
VerificationTemplatePath: "notfound.tmpl",
|
||||
MsgTemplatePath: "testdata/msg.html.tmpl",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with wrong path to message template",
|
||||
errText: "can't read message template: open notfound.tmpl: no such file or directory",
|
||||
errText: "notfound.tmpl: file does not exist",
|
||||
emailParams: EmailParams{
|
||||
VerificationTemplatePath: "testdata/verification.html.tmpl",
|
||||
MsgTemplatePath: "notfound.tmpl",
|
||||
MsgTemplatePath: "notfound.tmpl",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,8 +34,8 @@ type Destination interface {
|
||||
// Store defines the minimal interface accessing stored comments used by notifier
|
||||
type Store interface {
|
||||
Get(locator store.Locator, id string, user store.User) (store.Comment, error)
|
||||
GetUserEmail(siteID string, userID string) (string, error)
|
||||
GetUserTelegram(siteID string, userID string) (string, error)
|
||||
GetUserEmail(siteID, userID string) (string, error)
|
||||
GetUserTelegram(siteID, userID string) (string, error)
|
||||
}
|
||||
|
||||
// used for email and telegram retrieval from user details
|
||||
@@ -143,6 +143,12 @@ func (s *Service) SubmitVerification(req VerificationRequest) {
|
||||
// Close queue channel and wait for completion
|
||||
func (s *Service) Close() {
|
||||
if s.queue != nil {
|
||||
// don't panic in case service is already closed
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
log.Print("[DEBUG] close notifier")
|
||||
close(s.queue)
|
||||
close(s.verificationQueue)
|
||||
|
||||
@@ -21,6 +21,8 @@ func TestService_NoDestinations(t *testing.T) {
|
||||
s.Submit(Request{Comment: store.Comment{ID: "123"}})
|
||||
s.Submit(Request{Comment: store.Comment{ID: "123"}})
|
||||
s.Close()
|
||||
// second call should not result in panic
|
||||
s.Close()
|
||||
}
|
||||
|
||||
func TestService_WithDestinations(t *testing.T) {
|
||||
|
||||
@@ -29,15 +29,15 @@ type admin struct {
|
||||
|
||||
type adminStore interface {
|
||||
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error
|
||||
DeleteUser(siteID string, userID string, mode store.DeleteMode) error
|
||||
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error
|
||||
DeleteUser(siteID, userID string, mode store.DeleteMode) error
|
||||
DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
IsBlocked(siteID string, userID string) bool
|
||||
SetBlock(siteID string, userID string, status bool, ttl time.Duration) error
|
||||
IsBlocked(siteID, userID string) bool
|
||||
SetBlock(siteID, userID string, status bool, ttl time.Duration) error
|
||||
BlockedUsers(siteID string) ([]store.BlockedUser, error)
|
||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||
SetTitle(locator store.Locator, commentID string) (comment store.Comment, err error)
|
||||
SetVerified(siteID string, userID string, status bool) error
|
||||
SetVerified(siteID, userID string, status bool) error
|
||||
SetReadOnly(locator store.Locator, status bool) error
|
||||
SetPin(locator store.Locator, commentID string, status bool) error
|
||||
}
|
||||
|
||||
@@ -743,9 +743,9 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
|
||||
defer teardown()
|
||||
|
||||
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "remark42",
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "provider1_user1"}}
|
||||
c2 := store.Comment{Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "remark42",
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "user2"}}
|
||||
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user2", ID: "provider1_user2"}}
|
||||
|
||||
_, err := srv.DataService.Create(c1)
|
||||
assert.NoError(t, err)
|
||||
@@ -768,13 +768,13 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
|
||||
SessionOnly: true,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Audience: "remark42",
|
||||
Id: "1234567",
|
||||
Id: "provider1_1234567",
|
||||
Issuer: "remark42",
|
||||
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
||||
},
|
||||
User: &token.User{
|
||||
ID: "user1",
|
||||
ID: "provider1_user1",
|
||||
Attributes: map[string]interface{}{
|
||||
"delete_me": true,
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -164,7 +163,7 @@ func (m *Migrator) remapCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "remap failed, bad given rules", rest.ErrDecode)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
defer r.Body.Close() //nolint gosec // we don't care about response body
|
||||
|
||||
// start remap procedure with mapper
|
||||
go func() {
|
||||
@@ -172,7 +171,7 @@ func (m *Migrator) remapCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
defer m.setBusy(siteID, false)
|
||||
|
||||
// do export
|
||||
fh, e := ioutil.TempFile("", "remark42_convert")
|
||||
fh, e := os.CreateTemp("", "remark42_convert")
|
||||
if e != nil {
|
||||
log.Printf("[WARN] failed to make temp file %+v", e)
|
||||
return
|
||||
@@ -250,7 +249,7 @@ func (m *Migrator) runImport(siteID, provider, tmpfile string) {
|
||||
|
||||
// saveTemp reads from reader and saves to temp file
|
||||
func (m *Migrator) saveTemp(r io.Reader) (string, error) {
|
||||
tmpfile, err := ioutil.TempFile("", "remark42_import")
|
||||
tmpfile, err := os.CreateTemp("", "remark42_import")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't make temp file: %w", err)
|
||||
}
|
||||
|
||||
+116
-74
@@ -3,14 +3,19 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth/v6"
|
||||
"github.com/didip/tollbooth/v7"
|
||||
"github.com/didip/tollbooth_chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -21,7 +26,6 @@ import (
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/go-pkgz/rest/logger"
|
||||
"github.com/rakyll/statik/fs"
|
||||
|
||||
"github.com/umputun/remark42/backend/app/notify"
|
||||
"github.com/umputun/remark42/backend/app/rest"
|
||||
@@ -29,7 +33,6 @@ import (
|
||||
"github.com/umputun/remark42/backend/app/store"
|
||||
"github.com/umputun/remark42/backend/app/store/image"
|
||||
"github.com/umputun/remark42/backend/app/store/service"
|
||||
"github.com/umputun/remark42/backend/app/templates"
|
||||
)
|
||||
|
||||
// Rest is a rest access server
|
||||
@@ -48,6 +51,7 @@ type Rest struct {
|
||||
|
||||
AnonVote bool
|
||||
WebRoot string
|
||||
WebFS embed.FS
|
||||
RemarkURL string
|
||||
ReadOnlyAge int
|
||||
SharedSecret string
|
||||
@@ -55,16 +59,16 @@ type Rest struct {
|
||||
Low int
|
||||
Critical int
|
||||
}
|
||||
UpdateLimiter float64
|
||||
EmailNotifications bool
|
||||
TelegramBotUsername string
|
||||
EmojiEnabled bool
|
||||
SimpleView bool
|
||||
ProxyCORS bool
|
||||
SendJWTHeader bool
|
||||
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
||||
SubscribersOnly bool
|
||||
DisableSignature bool // prevent signature from being added to headers
|
||||
UpdateLimiter float64
|
||||
EmailNotifications bool
|
||||
TelegramNotifications bool
|
||||
EmojiEnabled bool
|
||||
SimpleView bool
|
||||
ProxyCORS bool
|
||||
SendJWTHeader bool
|
||||
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
||||
SubscribersOnly bool
|
||||
DisableSignature bool // prevent signature from being added to headers
|
||||
|
||||
SSLConfig SSLConfig
|
||||
httpsServer *http.Server
|
||||
@@ -223,7 +227,8 @@ func (s *Rest) routes() chi.Router {
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Timeout(5 * time.Second))
|
||||
r.Use(logInfoWithBody, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)), middleware.NoCache)
|
||||
r.Use(logInfoWithBody, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(2, nil)), middleware.NoCache)
|
||||
r.Use(validEmailAuth()) // reject suspicious email logins
|
||||
r.Mount("/auth", authHandler)
|
||||
})
|
||||
|
||||
@@ -344,14 +349,13 @@ func (s *Rest) routes() chi.Router {
|
||||
router.Group(func(rroot chi.Router) {
|
||||
rroot.Use(middleware.Timeout(10 * time.Second))
|
||||
rroot.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil)))
|
||||
rroot.Get("/index.html", s.pubRest.getStartedCtrl)
|
||||
rroot.Get("/robots.txt", s.pubRest.robotsCtrl)
|
||||
rroot.Get("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
|
||||
rroot.Post("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
|
||||
})
|
||||
|
||||
// file server for static content from /web
|
||||
addFileServer(router, "/web", http.Dir(s.WebRoot), s.Version)
|
||||
// file server for static content from s.WebRoot on path /web
|
||||
addFileServer(router, s.WebFS, s.WebRoot, s.Version)
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -362,7 +366,6 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
|
||||
imageService: s.ImageService,
|
||||
commentFormatter: s.CommentFormatter,
|
||||
readOnlyAge: s.ReadOnlyAge,
|
||||
webRoot: s.WebRoot,
|
||||
}
|
||||
|
||||
privGrp := private{
|
||||
@@ -376,7 +379,6 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
|
||||
telegramService: s.TelegramService,
|
||||
remarkURL: s.RemarkURL,
|
||||
anonVote: s.AnonVote,
|
||||
templates: templates.NewFS(),
|
||||
}
|
||||
|
||||
admGrp := admin{
|
||||
@@ -412,44 +414,44 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
emails, _ := s.DataService.AdminStore.Email(siteID)
|
||||
|
||||
cnf := struct {
|
||||
Version string `json:"version"`
|
||||
EditDuration int `json:"edit_duration"`
|
||||
AdminEdit bool `json:"admin_edit"`
|
||||
MaxCommentSize int `json:"max_comment_size"`
|
||||
Admins []string `json:"admins"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
Auth []string `json:"auth_providers"`
|
||||
AnonVote bool `json:"anon_vote"`
|
||||
LowScore int `json:"low_score"`
|
||||
CriticalScore int `json:"critical_score"`
|
||||
PositiveScore bool `json:"positive_score"`
|
||||
ReadOnlyAge int `json:"readonly_age"`
|
||||
MaxImageSize int `json:"max_image_size"`
|
||||
EmailNotifications bool `json:"email_notifications"`
|
||||
TelegramBotUsername string `json:"telegram_bot_username"`
|
||||
EmojiEnabled bool `json:"emoji_enabled"`
|
||||
SimpleView bool `json:"simple_view"`
|
||||
SendJWTHeader bool `json:"send_jwt_header"`
|
||||
SubscribersOnly bool `json:"subscribers_only"`
|
||||
Version string `json:"version"`
|
||||
EditDuration int `json:"edit_duration"`
|
||||
AdminEdit bool `json:"admin_edit"`
|
||||
MaxCommentSize int `json:"max_comment_size"`
|
||||
Admins []string `json:"admins"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
Auth []string `json:"auth_providers"`
|
||||
AnonVote bool `json:"anon_vote"`
|
||||
LowScore int `json:"low_score"`
|
||||
CriticalScore int `json:"critical_score"`
|
||||
PositiveScore bool `json:"positive_score"`
|
||||
ReadOnlyAge int `json:"readonly_age"`
|
||||
MaxImageSize int `json:"max_image_size"`
|
||||
EmailNotifications bool `json:"email_notifications"`
|
||||
TelegramNotifications bool `json:"telegram_notifications"`
|
||||
EmojiEnabled bool `json:"emoji_enabled"`
|
||||
SimpleView bool `json:"simple_view"`
|
||||
SendJWTHeader bool `json:"send_jwt_header"`
|
||||
SubscribersOnly bool `json:"subscribers_only"`
|
||||
}{
|
||||
Version: s.Version,
|
||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||
AdminEdit: s.DataService.AdminEdits,
|
||||
MaxCommentSize: s.DataService.MaxCommentSize,
|
||||
Admins: admins,
|
||||
AdminEmail: emails,
|
||||
LowScore: s.ScoreThresholds.Low,
|
||||
CriticalScore: s.ScoreThresholds.Critical,
|
||||
PositiveScore: s.DataService.PositiveScore,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
MaxImageSize: s.ImageService.MaxSize,
|
||||
EmailNotifications: s.EmailNotifications,
|
||||
TelegramBotUsername: s.TelegramBotUsername,
|
||||
EmojiEnabled: s.EmojiEnabled,
|
||||
AnonVote: s.AnonVote,
|
||||
SimpleView: s.SimpleView,
|
||||
SendJWTHeader: s.SendJWTHeader,
|
||||
SubscribersOnly: s.SubscribersOnly,
|
||||
Version: s.Version,
|
||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||
AdminEdit: s.DataService.AdminEdits,
|
||||
MaxCommentSize: s.DataService.MaxCommentSize,
|
||||
Admins: admins,
|
||||
AdminEmail: emails,
|
||||
LowScore: s.ScoreThresholds.Low,
|
||||
CriticalScore: s.ScoreThresholds.Critical,
|
||||
PositiveScore: s.DataService.PositiveScore,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
MaxImageSize: s.ImageService.MaxSize,
|
||||
EmailNotifications: s.EmailNotifications,
|
||||
TelegramNotifications: s.TelegramNotifications,
|
||||
EmojiEnabled: s.EmojiEnabled,
|
||||
AnonVote: s.AnonVote,
|
||||
SimpleView: s.SimpleView,
|
||||
SendJWTHeader: s.SendJWTHeader,
|
||||
SubscribersOnly: s.SubscribersOnly,
|
||||
}
|
||||
|
||||
cnf.Auth = []string{}
|
||||
@@ -464,34 +466,28 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, cnf)
|
||||
}
|
||||
|
||||
// serves static files from /web or embedded by statik
|
||||
func addFileServer(r chi.Router, path string, root http.FileSystem, version string) {
|
||||
// serves static files from the webRoot directory or files embedded into the compiled binary if that directory is absent
|
||||
func addFileServer(r chi.Router, embedFS embed.FS, webRoot, version string) {
|
||||
var webFS http.Handler
|
||||
|
||||
statikFS, err := fs.New()
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] no embedded assets loaded, %s", err)
|
||||
log.Printf("[INFO] run file server for %s, path %s", root, path)
|
||||
webFS = http.FileServer(root)
|
||||
if _, err := os.Stat(webRoot); err == nil {
|
||||
log.Printf("[INFO] run file server from %s from the disk", webRoot)
|
||||
webFS = http.FileServer(http.Dir(webRoot))
|
||||
} else {
|
||||
log.Printf("[INFO] run file server for %s, embedded", root)
|
||||
webFS = http.FileServer(statikFS)
|
||||
log.Printf("[INFO] run file server, embedded")
|
||||
var contentFS, _ = fs.Sub(embedFS, "web")
|
||||
webFS = http.FileServer(http.FS(contentFS))
|
||||
}
|
||||
|
||||
origPath := path
|
||||
webFS = http.StripPrefix(path, webFS)
|
||||
if path != "/" && path[len(path)-1] != '/' {
|
||||
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
||||
path += "/"
|
||||
}
|
||||
path += "*"
|
||||
webFS = http.StripPrefix("/web", webFS)
|
||||
r.Get("/web", http.RedirectHandler("/web/", http.StatusMovedPermanently).ServeHTTP)
|
||||
|
||||
r.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(20, nil)),
|
||||
middleware.Timeout(10*time.Second),
|
||||
cacheControl(time.Hour, version),
|
||||
).Get(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
).Get("/web/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
// don't show dirs, just serve files
|
||||
if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != (origPath+"/") {
|
||||
if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != ("/web/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
@@ -650,6 +646,52 @@ func subscribersOnly(enable bool) func(http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// validEmailAuth is a middleware for auth endpoints for email method.
|
||||
// it rejects login request if user, site or email are suspicious
|
||||
func validEmailAuth() func(http.Handler) http.Handler {
|
||||
|
||||
reUser := regexp.MustCompile(`^[\p{L}\d\s_]{4,64}$`) // matches ui side validation, adding min/max limitation
|
||||
reSite := regexp.MustCompile(`^[a-zA-Z\d\s_.-]{1,64}$`)
|
||||
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path != "/auth/email/login" {
|
||||
// not email login, skip the check
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if u := r.URL.Query().Get("user"); u != "" {
|
||||
if !reUser.MatchString(u) {
|
||||
log.Printf("[WARN] suspicious user rejected: %s", u)
|
||||
http.Error(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if a := r.URL.Query().Get("address"); a != "" {
|
||||
if _, err := mail.ParseAddress(a); err != nil {
|
||||
log.Printf("[WARN] suspicious address rejected: %s", a)
|
||||
http.Error(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if s := r.URL.Query().Get("site"); s != "" {
|
||||
if !reSite.MatchString(s) {
|
||||
log.Printf("[WARN] suspicious site rejected: %s", s)
|
||||
http.Error(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func parseError(err error, defaultCode int) (code int) {
|
||||
code = defaultCode
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ type private struct {
|
||||
telegramService telegramService
|
||||
remarkURL string
|
||||
anonVote bool
|
||||
templates templates.FileReader
|
||||
}
|
||||
|
||||
// telegramService is a subset of Telegram service used for setting up user telegram notifications
|
||||
@@ -59,15 +58,15 @@ type privStore interface {
|
||||
Vote(req service.VoteReq) (comment store.Comment, err error)
|
||||
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
GetUserEmail(siteID string, userID string) (string, error)
|
||||
SetUserEmail(siteID string, userID string, value string) (string, error)
|
||||
GetUserTelegram(siteID string, userID string) (string, error)
|
||||
SetUserTelegram(siteID string, userID string, value string) (string, error)
|
||||
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error
|
||||
GetUserEmail(siteID, userID string) (string, error)
|
||||
SetUserEmail(siteID, userID, value string) (string, error)
|
||||
GetUserTelegram(siteID, userID string) (string, error)
|
||||
SetUserTelegram(siteID, userID, value string) (string, error)
|
||||
DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
|
||||
ValidateComment(c *store.Comment) error
|
||||
IsVerified(siteID string, userID string) bool
|
||||
IsVerified(siteID, userID string) bool
|
||||
IsReadOnly(locator store.Locator) bool
|
||||
IsBlocked(siteID string, userID string) bool
|
||||
IsBlocked(siteID, userID string) bool
|
||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||
}
|
||||
|
||||
@@ -91,11 +90,11 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
comment = s.commentFormatter.Format(comment)
|
||||
comment.Sanitize()
|
||||
|
||||
// check if images are valid
|
||||
for _, id := range s.imageService.ExtractPictures(comment.Text) {
|
||||
// check if images are valid, omit proxied images as they are lazy-loaded
|
||||
for _, id := range s.imageService.ExtractNonProxiedPictures(comment.Text) {
|
||||
err := s.imageService.ResetCleanupTimer(id)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't renew staged picture cleanup timer", rest.ErrImgNotFound)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load picture from the comment", rest.ErrImgNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -130,8 +129,8 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
comment = s.commentFormatter.Format(comment)
|
||||
|
||||
// check if images are valid
|
||||
for _, id := range s.imageService.ExtractPictures(comment.Text) {
|
||||
// check if images are valid, omit proxied images as they are lazy-loaded
|
||||
for _, id := range s.imageService.ExtractNonProxiedPictures(comment.Text) {
|
||||
_, err := s.imageService.Load(id)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load picture from the comment", rest.ErrImgNotFound)
|
||||
@@ -173,7 +172,7 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
s.notifyService.Submit(notify.Request{Comment: finalComment})
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] created commend %+v", finalComment)
|
||||
log.Printf("[DEBUG] created comment %+v", finalComment)
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(w, r, &finalComment)
|
||||
@@ -188,7 +187,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}{}
|
||||
|
||||
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't read comment details from body", rest.ErrDecode)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -308,28 +307,59 @@ func (s *private) getEmailCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// sendEmailConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user.
|
||||
// GET /email/subscribe?site=siteID&address=someone@example.com
|
||||
// In case user is logged in with the same email, and auto_confirm is true, confirm it right away.
|
||||
// In case of quick confirmation, "updated" is set to true, otherwise - to false.
|
||||
// POST /email/subscribe with site and address in json body
|
||||
//
|
||||
//nolint:dupl // too hard to deduplicate that logic, as then it's tricky to use SendErrorJSON
|
||||
func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
user := rest.MustGetUserInfo(r)
|
||||
address := r.URL.Query().Get("address")
|
||||
siteID := r.URL.Query().Get("site")
|
||||
if address == "" {
|
||||
|
||||
subscribe := struct {
|
||||
Site string
|
||||
Address string
|
||||
autoConfirm bool
|
||||
}{autoConfirm: true}
|
||||
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &subscribe); err != nil {
|
||||
if err != io.EOF {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
|
||||
return
|
||||
}
|
||||
// old behavior fallback, reading from the query params. Auto confirm is false in this case.
|
||||
subscribe.Address = r.URL.Query().Get("address")
|
||||
subscribe.Site = r.URL.Query().Get("site")
|
||||
subscribe.autoConfirm = false
|
||||
}
|
||||
|
||||
if subscribe.Address == "" {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest,
|
||||
fmt.Errorf("missing parameter"), "address parameter is required", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
existingAddress, err := s.dataService.GetUserEmail(siteID, user.ID)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't read email for %s, %v", user.ID, err)
|
||||
existingAddress, getErr := s.dataService.GetUserEmail(subscribe.Site, user.ID)
|
||||
if getErr != nil {
|
||||
log.Printf("[WARN] can't read email for %s, %v", user.ID, getErr)
|
||||
}
|
||||
if address == existingAddress {
|
||||
if subscribe.Address == existingAddress {
|
||||
rest.SendErrorJSON(w, r, http.StatusConflict,
|
||||
fmt.Errorf("already verified"), "email address is already verified for this user", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
// in case the user logged in with the same email as they try to subscribe with, confirm it right away
|
||||
// this behavior is different from the previous one and is hidden behind the autoConfirm flag,
|
||||
// which is true for the new API, and false for the old one
|
||||
//
|
||||
// nolint:gosec // this is not used for security purposes
|
||||
if subscribe.autoConfirm &&
|
||||
strings.HasPrefix(user.ID, "email_") &&
|
||||
strings.TrimPrefix(user.ID, "email_") == token.HashID(sha1.New(), subscribe.Address) {
|
||||
s.setEmail(w, r, user.ID, subscribe.Site, subscribe.Address)
|
||||
return
|
||||
}
|
||||
|
||||
claims := token.Claims{
|
||||
Handshake: &token.Handshake{ID: user.ID + "::" + address},
|
||||
Handshake: &token.Handshake{ID: user.ID + "::" + subscribe.Address},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Audience: r.URL.Query().Get("site"),
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
||||
@@ -346,14 +376,14 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
s.notifyService.SubmitVerification(
|
||||
notify.VerificationRequest{
|
||||
SiteID: siteID,
|
||||
SiteID: subscribe.Site,
|
||||
User: user.Name,
|
||||
Email: address,
|
||||
Email: subscribe.Address,
|
||||
Token: tkn,
|
||||
},
|
||||
)
|
||||
|
||||
render.JSON(w, r, R.JSON{"user": user, "address": address})
|
||||
render.JSON(w, r, R.JSON{"user": user, "address": subscribe.Address, "updated": false})
|
||||
}
|
||||
|
||||
// telegramSubscribeCtrl generates and verifies telegram notification request
|
||||
@@ -400,7 +430,7 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request)
|
||||
var address, siteID string
|
||||
address, siteID, err := s.telegramService.CheckToken(queryToken, user.ID)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't set telegram for user", rest.ErrInternal)
|
||||
rest.SendErrorJSON(w, r, http.StatusNotFound, err, "request is not verified yet", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -417,17 +447,29 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token
|
||||
// PUT /email/confirm?site=siteID&tkn=jwt
|
||||
// POST /email/confirm with site and token in json body
|
||||
func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
tkn := r.URL.Query().Get("tkn")
|
||||
if tkn == "" {
|
||||
user := rest.MustGetUserInfo(r)
|
||||
|
||||
confirm := struct {
|
||||
Site string
|
||||
Token string
|
||||
}{}
|
||||
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &confirm); err != nil {
|
||||
if err != io.EOF {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
|
||||
return
|
||||
}
|
||||
// old behavior fallback, reading from the query params
|
||||
confirm.Token = r.URL.Query().Get("tkn")
|
||||
confirm.Site = r.URL.Query().Get("site")
|
||||
}
|
||||
|
||||
if confirm.Token == "" {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
user := rest.MustGetUserInfo(r)
|
||||
siteID := r.URL.Query().Get("site")
|
||||
|
||||
confClaims, err := s.authenticator.TokenService().Parse(tkn)
|
||||
confClaims, err := s.authenticator.TokenService().Parse(confirm.Token)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
||||
return
|
||||
@@ -445,17 +487,20 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
address := elems[1]
|
||||
s.setEmail(w, r, user.ID, confirm.Site, address)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] set email for user %s", user.ID)
|
||||
func (s *private) setEmail(w http.ResponseWriter, r *http.Request, userID, siteID, address string) {
|
||||
log.Printf("[DEBUG] set email for user %s", userID)
|
||||
|
||||
val, err := s.dataService.SetUserEmail(siteID, user.ID, address)
|
||||
val, err := s.dataService.SetUserEmail(siteID, userID, address)
|
||||
if err != nil {
|
||||
code := parseError(err, rest.ErrInternal)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set email for user", code)
|
||||
return
|
||||
}
|
||||
|
||||
// update User.Email from the token
|
||||
// update User.Email field
|
||||
claims, _, err := s.authenticator.TokenService().Get(r)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
||||
@@ -473,29 +518,26 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
|
||||
func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
tkn := r.URL.Query().Get("tkn")
|
||||
if tkn == "" {
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest,
|
||||
fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
siteID := r.URL.Query().Get("site")
|
||||
|
||||
confClaims, err := s.authenticator.TokenService().Parse(tkn)
|
||||
if err != nil {
|
||||
rest.SendErrorHTML(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
if s.authenticator.TokenService().IsExpired(confClaims) {
|
||||
rest.SendErrorHTML(w, r, http.StatusForbidden,
|
||||
fmt.Errorf("expired"), "failed to verify confirmation token", rest.ErrInternal, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusForbidden, fmt.Errorf("expired"), "failed to verify confirmation token", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
// Handshake.ID is user.ID + "::" + address
|
||||
elems := strings.Split(confClaims.Handshake.ID, "::")
|
||||
if len(elems) != 2 {
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest,
|
||||
fmt.Errorf("%s", confClaims.Handshake.ID), "invalid handshake token", rest.ErrInternal, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("%s", confClaims.Handshake.ID), "invalid handshake token", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
userID := elems[0]
|
||||
@@ -508,14 +550,11 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[WARN] can't read email for %s, %v", userID, err)
|
||||
}
|
||||
if existingAddress == "" {
|
||||
rest.SendErrorHTML(w, r, http.StatusConflict,
|
||||
fmt.Errorf("user is not subscribed"), "user does not have active email subscription", rest.ErrInternal, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusConflict, fmt.Errorf("user is not subscribed"), "user does not have active email subscription", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
if address != existingAddress {
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest,
|
||||
fmt.Errorf("wrong email unsubscription"), "email address in request does not match known for this user",
|
||||
rest.ErrInternal, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("wrong email unsubscription"), "email address in request does not match known for this user", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -523,7 +562,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err = s.dataService.DeleteUserDetail(siteID, userID, engine.UserEmail); err != nil {
|
||||
code := parseError(err, rest.ErrInternal)
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest, err, "can't delete email for user", code, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusBadRequest, err, "can't delete email for user", code)
|
||||
return
|
||||
}
|
||||
// clean User.Email from the token, if user has the token
|
||||
@@ -534,7 +573,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
if claims.User != nil && claims.User.Email != "" {
|
||||
claims.User.Email = ""
|
||||
if _, err = s.authenticator.TokenService().Set(w, claims); err != nil {
|
||||
rest.SendErrorHTML(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal, s.templates)
|
||||
rest.SendErrorHTML(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -546,13 +585,13 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
MustRead := func(path string) string {
|
||||
file, err := s.templates.ReadFile(path)
|
||||
file, err := templates.Read(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(file)
|
||||
}
|
||||
tmplstr := MustRead("unsubscribe.html.tmpl")
|
||||
tmplstr := MustRead("email_unsubscribe.html.tmpl")
|
||||
tmpl := template.Must(template.New("unsubscribe").Parse(tmplstr))
|
||||
msg := bytes.Buffer{}
|
||||
MustExecute(tmpl, &msg, nil)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/remark42/backend/app/notify"
|
||||
"github.com/umputun/remark42/backend/app/rest/proxy"
|
||||
"github.com/umputun/remark42/backend/app/store"
|
||||
"github.com/umputun/remark42/backend/app/store/image"
|
||||
)
|
||||
@@ -54,6 +56,111 @@ func TestRest_Create(t *testing.T) {
|
||||
assert.True(t, len(c["id"].(string)) > 8)
|
||||
}
|
||||
|
||||
// based on issue https://github.com/umputun/remark42/issues/1292
|
||||
func TestRest_CreateFilteredCode(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
resp, err := post(t, ts.URL+"/api/v1/comment",
|
||||
`{"text": "`+"`foo<bar>`"+`", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||
assert.NoError(t, err)
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode, string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
c := R.JSON{}
|
||||
err = json.Unmarshal(b, &c)
|
||||
require.NoError(t, err, string(b))
|
||||
loc := c["locator"].(map[string]interface{})
|
||||
assert.Equal(t, "remark42", loc["site"])
|
||||
assert.Equal(t, "https://radio-t.com/blah1", loc["url"])
|
||||
assert.Equal(t, "`foo<bar>`", c["orig"])
|
||||
assert.Contains(t, c["text"], "foo")
|
||||
assert.Contains(t, c["text"], "bar")
|
||||
assert.True(t, len(c["id"].(string)) > 8)
|
||||
}
|
||||
|
||||
// based on issue https://github.com/umputun/remark42/issues/1631
|
||||
func TestRest_CreateAndPreviewWithImage(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
ts.Close()
|
||||
defer teardown()
|
||||
|
||||
srv.ImageService.ProxyAPI = srv.RemarkURL + "/api/v1/img"
|
||||
srv.ImageProxy = &proxy.Image{
|
||||
HTTP2HTTPS: true,
|
||||
CacheExternal: true,
|
||||
RoutePath: "/api/v1/img",
|
||||
RemarkURL: srv.RemarkURL,
|
||||
ImageService: srv.ImageService,
|
||||
}
|
||||
srv.CommentFormatter = store.NewCommentFormatter(srv.ImageProxy)
|
||||
// need to recreate the server with new ImageProxy, otherwise old one will be used
|
||||
ts = httptest.NewServer(srv.routes())
|
||||
defer ts.Close()
|
||||
|
||||
var pngRead bool
|
||||
// server with the test PNG image
|
||||
pngServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, e := io.Copy(w, gopherPNG())
|
||||
assert.NoError(t, e)
|
||||
pngRead = true
|
||||
}))
|
||||
defer pngServer.Close()
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
resp, err := post(t, ts.URL+"/api/v1/comment",
|
||||
`{"text": "", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||
assert.NoError(t, err)
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode, string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
c := R.JSON{}
|
||||
err = json.Unmarshal(b, &c)
|
||||
require.NoError(t, err, string(b))
|
||||
assert.NotContains(t, c["text"], pngServer.URL)
|
||||
assert.Contains(t, c["text"], srv.RemarkURL)
|
||||
loc := c["locator"].(map[string]interface{})
|
||||
assert.Equal(t, "remark42", loc["site"])
|
||||
assert.Equal(t, "https://radio-t.com/blah1", loc["url"])
|
||||
assert.True(t, len(c["id"].(string)) > 8)
|
||||
assert.Equal(t, false, pngRead, "original image is not yet accessed by server")
|
||||
})
|
||||
|
||||
t.Run("preview", func(t *testing.T) {
|
||||
resp, err := post(t, ts.URL+"/api/v1/preview",
|
||||
`{"text": "", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||
assert.NoError(t, err)
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
|
||||
assert.NotContains(t, string(b), pngServer.URL)
|
||||
assert.Contains(t, string(b), srv.RemarkURL)
|
||||
|
||||
assert.Equal(t, false, pngRead, "original image is not yet accessed by server")
|
||||
// retrieve the image from the cache
|
||||
imgURL := strings.Split(strings.Split(string(b), "src=\"")[1], "\"")[0]
|
||||
// replace srv.RemarkURL with ts.URL
|
||||
imgURL = strings.ReplaceAll(imgURL, srv.RemarkURL, ts.URL)
|
||||
resp, err = http.Get(imgURL)
|
||||
assert.NoError(t, err)
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
// compare image to original gopher png after decoding from base64
|
||||
assert.Equal(t, gopher, base64.StdEncoding.EncodeToString(b))
|
||||
|
||||
assert.Equal(t, true, pngRead, "original image accessed to be shown to user")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestRest_CreateOldPost(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
@@ -141,6 +248,25 @@ func TestRest_CreateWithRestrictedWord(t *testing.T) {
|
||||
assert.Equal(t, "invalid comment", c["details"])
|
||||
}
|
||||
|
||||
func TestRest_CreateRelativeURL(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
// check that it's not possible to click insert URL button and not alter the URL in it (which is `url` by default)
|
||||
relativeURLText := `{"text": "here is a link with relative URL: [google.com](url)", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`
|
||||
resp, err := post(t, ts.URL+"/api/v1/comment", relativeURLText)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
c := R.JSON{}
|
||||
err = json.Unmarshal(b, &c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "links should start with mailto:, http:// or https://", c["error"])
|
||||
assert.Equal(t, "invalid comment", c["details"])
|
||||
}
|
||||
|
||||
func TestRest_CreateRejected(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
@@ -348,6 +474,109 @@ func TestRest_UpdateDelete(t *testing.T) {
|
||||
{URL: "https://radio-t.com/blah2", Count: 0}}, j)
|
||||
}
|
||||
|
||||
func TestRest_DeleteChildThenParent(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
p := store.Comment{Text: "test test #1",
|
||||
Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah1"}}
|
||||
idP := addComment(t, p, ts)
|
||||
|
||||
c1 := store.Comment{Text: "test test #1", ParentID: idP,
|
||||
Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah1"}}
|
||||
idC1 := addComment(t, c1, ts)
|
||||
|
||||
c2 := store.Comment{Text: "test test #1", ParentID: idP,
|
||||
Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah1"}}
|
||||
idC2 := addComment(t, c2, ts)
|
||||
|
||||
// check multi count equals two
|
||||
resp, err := post(t, ts.URL+"/api/v1/counts?site=remark42", `["https://radio-t.com/blah1"]`)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
bb, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
j := []store.PostInfo{}
|
||||
err = json.Unmarshal(bb, &j)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 3}}, j)
|
||||
|
||||
// update a parent comment fails after child is created
|
||||
client := http.Client{}
|
||||
defer client.CloseIdleConnections()
|
||||
req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idP+"?site=remark42&url=https://radio-t.com/blah1",
|
||||
strings.NewReader(`{"text": "updated text", "summary":"updated by user"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
b, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(b.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, b.StatusCode, string(body))
|
||||
assert.NoError(t, b.Body.Close())
|
||||
|
||||
// delete first child comment
|
||||
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idC1+"?site=remark42&url=https://radio-t.com/blah1",
|
||||
strings.NewReader(`{"delete": true, "summary":"removed by user"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
b, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(b.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, b.StatusCode, string(body))
|
||||
assert.NoError(t, b.Body.Close())
|
||||
|
||||
// delete a parent comment, fails as one comment child still present
|
||||
defer client.CloseIdleConnections()
|
||||
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idP+"?site=remark42&url=https://radio-t.com/blah1",
|
||||
strings.NewReader(`{"delete": true, "summary":"removed by user"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
b, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(b.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, b.StatusCode, string(body))
|
||||
assert.NoError(t, b.Body.Close())
|
||||
|
||||
// delete second child comment, as an admin to check both deletion methods
|
||||
req, err = http.NewRequest(http.MethodDelete,
|
||||
fmt.Sprintf("%s/api/v1/admin/comment/%s?site=remark42&url=https://radio-t.com/blah1", ts.URL, idC2), http.NoBody)
|
||||
require.NoError(t, err)
|
||||
requireAdminOnly(t, req)
|
||||
resp, err = sendReq(t, req, adminUmputunToken)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// delete a parent comment, shouldn't fail after children are deleted
|
||||
defer client.CloseIdleConnections()
|
||||
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idP+"?site=remark42&url=https://radio-t.com/blah1",
|
||||
strings.NewReader(`{"delete": true, "summary":"removed by user"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
b, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(b.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, b.StatusCode, string(body))
|
||||
assert.NoError(t, b.Body.Close())
|
||||
|
||||
// check multi count decremented to zero
|
||||
resp, err = post(t, ts.URL+"/api/v1/counts?site=remark42", `["https://radio-t.com/blah1"]`)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
bb, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
j = []store.PostInfo{}
|
||||
err = json.Unmarshal(bb, &j)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 0}}, j)
|
||||
}
|
||||
|
||||
func TestRest_UpdateNotOwner(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
@@ -371,8 +600,6 @@ func TestRest_UpdateNotOwner(t *testing.T) {
|
||||
assert.Equal(t, http.StatusForbidden, b.StatusCode, string(body), "update from non-owner")
|
||||
assert.Equal(t, `{"code":3,"details":"can not edit comments for other users","error":"rejected"}`+"\n", string(body))
|
||||
|
||||
client = http.Client{}
|
||||
defer client.CloseIdleConnections()
|
||||
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+
|
||||
"?site=remark42&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`))
|
||||
assert.NoError(t, err)
|
||||
@@ -448,7 +675,7 @@ func TestRest_Vote(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("%s/api/v1/vote/%s?site=remark42&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), http.NoBody)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
req.Header.Add("X-JWT", dev2Token)
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
@@ -457,7 +684,7 @@ func TestRest_Vote(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, vote(1), "first vote allowed")
|
||||
assert.Equal(t, http.StatusBadRequest, vote(1), "second vote rejected")
|
||||
body, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
body, code := getWithDev2Auth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
cr := store.Comment{}
|
||||
err := json.Unmarshal([]byte(body), &cr)
|
||||
@@ -468,7 +695,7 @@ func TestRest_Vote(t *testing.T) {
|
||||
assert.Equal(t, map[string]store.VotedIPInfo(nil), cr.VotedIPs, "hidden")
|
||||
|
||||
assert.Equal(t, http.StatusOK, vote(-1), "opposite vote allowed")
|
||||
body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
body, code = getWithDev2Auth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
cr = store.Comment{}
|
||||
err = json.Unmarshal([]byte(body), &cr)
|
||||
@@ -477,7 +704,7 @@ func TestRest_Vote(t *testing.T) {
|
||||
assert.Equal(t, 0, cr.Vote)
|
||||
|
||||
assert.Equal(t, http.StatusOK, vote(-1), "opposite vote allowed one more time")
|
||||
body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
body, code = getWithDev2Auth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
cr = store.Comment{}
|
||||
err = json.Unmarshal([]byte(body), &cr)
|
||||
@@ -486,7 +713,7 @@ func TestRest_Vote(t *testing.T) {
|
||||
assert.Equal(t, -1, cr.Vote)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, vote(-1), "dbl vote not allowed")
|
||||
body, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
body, code = getWithDev2Auth(t, fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1))
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
cr = store.Comment{}
|
||||
err = json.Unmarshal([]byte(body), &cr)
|
||||
@@ -575,22 +802,15 @@ func TestRest_AnonVote(t *testing.T) {
|
||||
assert.Equal(t, map[string]store.VotedIPInfo(nil), cr.VotedIPs)
|
||||
}
|
||||
|
||||
type MockFS struct{}
|
||||
|
||||
func (fs *MockFS) ReadFile(path string) ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("template %s", path)), nil
|
||||
}
|
||||
|
||||
func TestRest_EmailAndTelegram(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
srv.privRest.templates = &MockFS{}
|
||||
srv.privRest.telegramService = &mockTelegram{site: "remark42"}
|
||||
|
||||
// issue good token
|
||||
claims := token.Claims{
|
||||
Handshake: &token.Handshake{ID: "dev::good@example.com"},
|
||||
Handshake: &token.Handshake{ID: "provider1_dev::good@example.com"},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Audience: "remark42",
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute).Unix(),
|
||||
@@ -609,19 +829,25 @@ func TestRest_EmailAndTelegram(t *testing.T) {
|
||||
responseCode int
|
||||
noAuth bool
|
||||
cookieEmail string
|
||||
body string
|
||||
}{
|
||||
{description: "issue delete request without auth", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusUnauthorized, noAuth: true},
|
||||
{description: "issue delete request without site_id", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusBadRequest},
|
||||
{description: "delete non-existent user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
|
||||
{description: "set user email, token not set", url: "/api/v1/email/confirm?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||
{description: "send email confirmation without address", url: "/api/v1/email/subscribe?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||
{description: "send email confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
|
||||
{description: "set user email, token is good", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
||||
{description: "set user email, token not set", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusBadRequest, body: `{"site":"remark42"}`},
|
||||
{description: "set user email, token not set, old query param", url: "/api/v1/email/confirm?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||
{description: "send email confirmation without address", url: "/api/v1/email/subscribe", method: http.MethodPost, responseCode: http.StatusBadRequest, body: `{"site":"remark42"}`},
|
||||
{description: "send email confirmation without address, old query param", url: "/api/v1/email/subscribe?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||
{description: "send email confirmation", url: "/api/v1/email/subscribe", method: http.MethodPost, responseCode: http.StatusOK, body: `{"site":"remark42","address":"good@example.com"}`},
|
||||
{description: "send email confirmation, old query param", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
|
||||
{description: "set user email, token is good", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com", body: fmt.Sprintf(`{"site":"remark42","token":%q}`, goodToken)},
|
||||
{description: "set user email, token is good, old query param", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
||||
{description: "send confirmation with same address", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusConflict},
|
||||
{description: "get user email", url: "/api/v1/email?site=remark42", method: http.MethodGet, responseCode: http.StatusOK},
|
||||
{description: "delete user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
|
||||
{description: "send another confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
|
||||
{description: "set user email, token is good", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
||||
{description: "set user email, token is good", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com", body: fmt.Sprintf(`{"site":"remark42","token":%q}`, goodToken)},
|
||||
{description: "set user email, token is good, old query param", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
||||
{description: "unsubscribe user, no token", url: "/email/unsubscribe.html?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||
{description: "unsubscribe user, wrong token", url: "/email/unsubscribe.html?site=remark42&tkn=jwt", method: http.MethodGet, responseCode: http.StatusForbidden},
|
||||
{description: "unsubscribe user, good token", url: fmt.Sprintf("/email/unsubscribe.html?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK},
|
||||
@@ -642,7 +868,11 @@ func TestRest_EmailAndTelegram(t *testing.T) {
|
||||
for _, x := range testData {
|
||||
x := x
|
||||
t.Run(x.description, func(t *testing.T) {
|
||||
req, err := http.NewRequest(x.method, ts.URL+x.url, http.NoBody)
|
||||
reqBody := io.NopCloser(strings.NewReader(x.body))
|
||||
if x.body == "" {
|
||||
reqBody = http.NoBody
|
||||
}
|
||||
req, err := http.NewRequest(x.method, ts.URL+x.url, reqBody)
|
||||
require.NoError(t, err)
|
||||
if !x.noAuth {
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
@@ -679,7 +909,7 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
// create new comment from dev user
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(
|
||||
`{"text": "test 123",
|
||||
"user": {"name": "dev::good@example.com"},
|
||||
"user": {"name": "provider1_dev::good@example.com"},
|
||||
"locator":{"url": "https://radio-t.com/blah1",
|
||||
"site": "remark42"}}`))
|
||||
assert.NoError(t, err)
|
||||
@@ -718,7 +948,11 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
assert.Empty(t, mockDestination.Get()[1].Emails)
|
||||
|
||||
// send confirmation token for email
|
||||
req, err = http.NewRequest(http.MethodPost, ts.URL+"/api/v1/email/subscribe?site=remark42&address=good@example.com", http.NoBody)
|
||||
req, err = http.NewRequest(
|
||||
http.MethodPost,
|
||||
ts.URL+"/api/v1/email/subscribe",
|
||||
io.NopCloser(strings.NewReader(`{"site": "remark42", "address": "good@example.com"}`)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
resp, err = client.Do(req)
|
||||
@@ -733,8 +967,31 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
assert.Equal(t, "good@example.com", mockDestination.GetVerify()[0].Email)
|
||||
verificationToken := mockDestination.GetVerify()[0].Token
|
||||
|
||||
// get user information to verify lack of the subscription
|
||||
req, err = http.NewRequest(
|
||||
http.MethodGet,
|
||||
ts.URL+"/api/v1/user?site=remark42",
|
||||
http.NoBody)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
var clearUser store.User
|
||||
err = json.Unmarshal(body, &clearUser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "provider1_dev", EmailSubscription: false,
|
||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, clearUser)
|
||||
|
||||
// verify email
|
||||
req, err = http.NewRequest(http.MethodPost, ts.URL+fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", verificationToken), http.NoBody)
|
||||
req, err = http.NewRequest(
|
||||
http.MethodPost,
|
||||
ts.URL+"/api/v1/email/confirm",
|
||||
io.NopCloser(strings.NewReader(fmt.Sprintf(`{"site": "remark42", "token": %q}`, verificationToken))),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
resp, err = client.Do(req)
|
||||
@@ -745,7 +1002,10 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
|
||||
// get user information to verify the subscription
|
||||
req, err = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/user?site=remark42", http.NoBody)
|
||||
req, err = http.NewRequest(
|
||||
http.MethodGet,
|
||||
ts.URL+"/api/v1/user?site=remark42",
|
||||
http.NoBody)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", devToken)
|
||||
resp, err = client.Do(req)
|
||||
@@ -754,11 +1014,11 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
var user store.User
|
||||
err = json.Unmarshal(body, &user)
|
||||
var subscribedUser store.User
|
||||
err = json.Unmarshal(body, &subscribedUser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: true,
|
||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, user)
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "provider1_dev", EmailSubscription: true,
|
||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, subscribedUser)
|
||||
|
||||
// create child comment from another user, email notification expected
|
||||
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(fmt.Sprintf(
|
||||
@@ -809,6 +1069,80 @@ func TestRest_EmailNotification(t *testing.T) {
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 4, len(mockDestination.Get()))
|
||||
assert.Empty(t, mockDestination.Get()[3].Emails)
|
||||
|
||||
// confirm email via subscribe call with query params, old behavior, email notification is expected
|
||||
req, err = http.NewRequest(
|
||||
http.MethodPost,
|
||||
ts.URL+"/api/v1/email/subscribe?site=remark42&address=good@example.com",
|
||||
http.NoBody,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", emailUserToken)
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
// wait for mock notification Submit to kick off
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 2, len(mockDestination.GetVerify()), "verification email was sent")
|
||||
|
||||
// get email user information to verify there is no subscription yet
|
||||
req, err = http.NewRequest(
|
||||
http.MethodGet,
|
||||
ts.URL+"/api/v1/user?site=remark42",
|
||||
http.NoBody)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", emailUserToken)
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
var unsubscribedEmailUser store.User
|
||||
err = json.Unmarshal(body, &unsubscribedEmailUser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.User{Name: "good@example.com test user", ID: "email_f5dfe9d2e6bd75fc74ea5fabf273b45b5baeb195", EmailSubscription: false,
|
||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, unsubscribedEmailUser)
|
||||
|
||||
// confirm email via subscribe call, no email notification is expected
|
||||
req, err = http.NewRequest(
|
||||
http.MethodPost,
|
||||
ts.URL+"/api/v1/email/subscribe",
|
||||
io.NopCloser(strings.NewReader(`{"site": "remark42", "address": "good@example.com"}`)),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", emailUserToken)
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
// wait for mock notification Submit to kick off
|
||||
time.Sleep(time.Millisecond * 30)
|
||||
require.Equal(t, 2, len(mockDestination.GetVerify()), "no new verification email was sent")
|
||||
|
||||
// get email user information to verify the subscription happened without the confirmation call
|
||||
req, err = http.NewRequest(
|
||||
http.MethodGet,
|
||||
ts.URL+"/api/v1/user?site=remark42",
|
||||
http.NoBody)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", emailUserToken)
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||
var subscribedEmailUser store.User
|
||||
err = json.Unmarshal(body, &subscribedEmailUser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.User{Name: "good@example.com test user", ID: "email_f5dfe9d2e6bd75fc74ea5fabf273b45b5baeb195", EmailSubscription: true,
|
||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, subscribedEmailUser)
|
||||
}
|
||||
|
||||
func TestRest_TelegramNotification(t *testing.T) {
|
||||
@@ -825,7 +1159,7 @@ func TestRest_TelegramNotification(t *testing.T) {
|
||||
// create new comment from dev user
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(
|
||||
`{"text": "test 123",
|
||||
"user": {"name": "dev::good@example.com"},
|
||||
"user": {"name": "provider1_dev::good@example.com"},
|
||||
"locator":{"url": "https://radio-t.com/blah1",
|
||||
"site": "remark42"}}`))
|
||||
assert.NoError(t, err)
|
||||
@@ -904,8 +1238,8 @@ func TestRest_TelegramNotification(t *testing.T) {
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusInternalServerError, resp.StatusCode, string(body))
|
||||
require.Equal(t, `{"code":0,"details":"can't set telegram for user","error":"not verified"}`+"\n", string(body))
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode, string(body))
|
||||
require.Equal(t, `{"code":0,"details":"request is not verified yet","error":"not verified"}`+"\n", string(body))
|
||||
|
||||
mockTlgrm.notVerified = false
|
||||
|
||||
@@ -953,7 +1287,7 @@ func TestRest_TelegramNotification(t *testing.T) {
|
||||
var user store.User
|
||||
err = json.Unmarshal(body, &user)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "dev",
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "provider1_dev",
|
||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, user)
|
||||
|
||||
// create child comment from another user, telegram notification expected
|
||||
@@ -1012,7 +1346,7 @@ func TestRest_UserAllData(t *testing.T) {
|
||||
defer teardown()
|
||||
|
||||
// write 3 comments
|
||||
user := store.User{ID: "dev", Name: "user name 1"}
|
||||
user := store.User{ID: "provider1_dev", Name: "user name 1"}
|
||||
c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "remark42",
|
||||
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 5, 27, 1, 14, 10, 0, time.Local)}
|
||||
c2 := store.Comment{User: user, Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "remark42",
|
||||
@@ -1037,13 +1371,13 @@ func TestRest_UserAllData(t *testing.T) {
|
||||
require.Equal(t, "application/gzip", resp.Header.Get("Content-Type"))
|
||||
|
||||
ungzReader, err := gzip.NewReader(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
ungzBody, err := io.ReadAll(ungzReader)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
strUungzBody := string(ungzBody)
|
||||
assert.True(t, strings.HasPrefix(strUungzBody,
|
||||
`{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false,"site_id":"remark42"}, "comments":[{`))
|
||||
`{"info": {"name":"developer one","id":"provider1_dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false,"site_id":"remark42"}, "comments":[{`))
|
||||
assert.Equal(t, 3, strings.Count(strUungzBody, `"text":`), "3 comments inside")
|
||||
|
||||
parsed := struct {
|
||||
@@ -1053,7 +1387,7 @@ func TestRest_UserAllData(t *testing.T) {
|
||||
|
||||
err = json.Unmarshal(ungzBody, &parsed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "dev",
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "provider1_dev",
|
||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, parsed.Info)
|
||||
assert.Equal(t, 3, len(parsed.Comments))
|
||||
|
||||
@@ -1069,7 +1403,7 @@ func TestRest_UserAllDataManyComments(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
user := store.User{ID: "dev", Name: "user name 1"}
|
||||
user := store.User{ID: "provider1_dev", Name: "user name 1"}
|
||||
c := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "remark42",
|
||||
URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 5, 27, 1, 14, 10, 0, time.Local)}
|
||||
|
||||
@@ -1096,7 +1430,7 @@ func TestRest_UserAllDataManyComments(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
strUngzBody := string(ungzBody)
|
||||
assert.True(t, strings.HasPrefix(strUngzBody,
|
||||
`{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false,"site_id":"remark42"}, "comments":[{`))
|
||||
`{"info": {"name":"developer one","id":"provider1_dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false,"site_id":"remark42"}, "comments":[{`))
|
||||
assert.Equal(t, 51, strings.Count(strUngzBody, `"text":`), "51 comments inside")
|
||||
}
|
||||
|
||||
@@ -1120,12 +1454,12 @@ func TestRest_DeleteMe(t *testing.T) {
|
||||
err = json.Unmarshal(body, &m)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "remark42", m["site"])
|
||||
assert.Equal(t, "dev", m["user_id"])
|
||||
assert.Equal(t, "provider1_dev", m["user_id"])
|
||||
|
||||
tkn := m["token"]
|
||||
claims, err := srv.Authenticator.TokenService().Parse(tkn)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "dev", claims.User.ID)
|
||||
assert.Equal(t, "provider1_dev", claims.User.ID)
|
||||
assert.Equal(t, "https://demo.remark42.com/web/deleteme.html?token="+tkn, m["link"])
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=remark42", ts.URL), http.NoBody)
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -32,7 +30,6 @@ type public struct {
|
||||
readOnlyAge int
|
||||
commentFormatter *store.CommentFormatter
|
||||
imageService *image.Service
|
||||
webRoot string
|
||||
}
|
||||
|
||||
type pubStore interface {
|
||||
@@ -43,7 +40,7 @@ type pubStore interface {
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
UserCount(siteID, userID string) (int, error)
|
||||
Count(locator store.Locator) (int, error)
|
||||
List(siteID string, limit int, skip int) ([]store.PostInfo, error)
|
||||
List(siteID string, limit, skip int) ([]store.PostInfo, error)
|
||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||
|
||||
ValidateComment(c *store.Comment) error
|
||||
@@ -345,16 +342,6 @@ func (s *public) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// GET /index.html - respond to /index.html with the content of getstarted.html under /web root
|
||||
func (s *public) getStartedCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := os.ReadFile(path.Join(s.webRoot, "getstarted.html"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
render.HTML(w, r, string(data))
|
||||
}
|
||||
|
||||
// GET /robots.txt
|
||||
func (s *public) robotsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/user",
|
||||
|
||||
@@ -76,8 +76,8 @@ func TestRest_Preview(t *testing.T) {
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Contains(t,
|
||||
string(b),
|
||||
"{\"code\":20,\"details\":\"can't renew staged picture cleanup timer\","+
|
||||
"\"error\":\"can't get image stats for dev_user/bad_picture: stat",
|
||||
`{"code":20,"details":"can't load picture from the comment",`+
|
||||
`"error":"can't get image stats for dev_user/bad_picture: stat`,
|
||||
)
|
||||
assert.Contains(t,
|
||||
string(b),
|
||||
@@ -97,8 +97,8 @@ func TestRest_PreviewWithWrongImage(t *testing.T) {
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Contains(t,
|
||||
string(b),
|
||||
"{\"code\":20,\"details\":\"can't renew staged picture cleanup timer\","+
|
||||
"\"error\":\"can't get image stats for dev_user/bad_picture: stat ",
|
||||
`{"code":20,"details":"can't load picture from the comment",`+
|
||||
`"error":"can't get image stats for dev_user/bad_picture: stat `,
|
||||
)
|
||||
assert.Contains(t,
|
||||
string(b),
|
||||
@@ -131,10 +131,10 @@ BKT
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<h1>h1</h1>
|
||||
<pre class="chroma"><code><span><span>func TestRest_Preview(t *testing.T) {
|
||||
</span></span><span><span>srv, ts := prep(t)
|
||||
</span></span><span><span> require.NotNil(t, srv)
|
||||
</span></span><span><span>}
|
||||
<pre class="chroma"><code><span class="line"><span class="cl">func TestRest_Preview(t *testing.T) {
|
||||
</span></span><span class="line"><span class="cl">srv, ts := prep(t)
|
||||
</span></span><span class="line"><span class="cl"> require.NotNil(t, srv)
|
||||
</span></span><span class="line"><span class="cl">}
|
||||
</span></span></code></pre>`,
|
||||
string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
@@ -157,7 +157,7 @@ BKT
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<pre class="chroma"><code><span><span><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
||||
assert.Equal(t, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
||||
</span></span></code></pre>`, string(b))
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
}
|
||||
@@ -325,8 +325,8 @@ func TestRest_FindUserView(t *testing.T) {
|
||||
require.Equal(t, 2, len(comments.Comments), "should have 2 comments")
|
||||
assert.Equal(t, id1, comments.Comments[0].ID)
|
||||
assert.Equal(t, id2, comments.Comments[1].ID)
|
||||
assert.Equal(t, "dev", comments.Comments[0].User.ID)
|
||||
assert.Equal(t, "dev", comments.Comments[1].User.ID)
|
||||
assert.Equal(t, "provider1_dev", comments.Comments[0].User.ID)
|
||||
assert.Equal(t, "provider1_dev", comments.Comments[1].User.ID)
|
||||
assert.Equal(t, "", comments.Comments[0].Text)
|
||||
assert.Equal(t, "", comments.Comments[1].Text)
|
||||
|
||||
@@ -440,7 +440,7 @@ func TestRest_FindUserComments(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, code, "noting for user blah")
|
||||
assert.Equal(t, `{"comments":[],"count":0}`+"\n", comments)
|
||||
{
|
||||
res, code := get(t, ts.URL+"/api/v1/comments?site=remark42&user=dev")
|
||||
res, code := get(t, ts.URL+"/api/v1/comments?site=remark42&user=provider1_dev")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
|
||||
resp := struct {
|
||||
@@ -459,7 +459,7 @@ func TestRest_FindUserComments(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
res, code := get(t, ts.URL+"/api/v1/comments?site=remark42&user=dev&skip=1&limit=2")
|
||||
res, code := get(t, ts.URL+"/api/v1/comments?site=remark42&user=provider1_dev&skip=1&limit=2")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
|
||||
resp := struct {
|
||||
@@ -486,7 +486,7 @@ func TestRest_UserInfo(t *testing.T) {
|
||||
user := store.User{}
|
||||
err := json.Unmarshal([]byte(body), &user)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "http://example.com/pic.png",
|
||||
assert.Equal(t, store.User{Name: "developer one", ID: "provider1_dev", Picture: "http://example.com/pic.png",
|
||||
IP: "127.0.0.1", SiteID: "remark42"}, user)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/go-pkgz/auth"
|
||||
"github.com/go-pkgz/auth/avatar"
|
||||
"github.com/go-pkgz/auth/provider"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
R "github.com/go-pkgz/rest"
|
||||
@@ -37,11 +38,18 @@ import (
|
||||
"github.com/umputun/remark42/backend/app/store/service"
|
||||
)
|
||||
|
||||
var devToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg`
|
||||
// To generate a token, enter one of the tokens here into https://jwt.io, change the secret to one you're using in your test
|
||||
// ("secret" in case of startupT), and alter the fields you want to be changed.
|
||||
|
||||
var devToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6InByb3ZpZGVyMV9kZXYiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20ifX0.dirTS_ahSF6375sdO2iodm2K2UmRTzQNQMFiHuTQCVs`
|
||||
|
||||
var dev2Token = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6InByb3ZpZGVyMV9kZXYyIiwicGljdHVyZSI6Imh0dHA6Ly9leGFtcGxlLmNvbS9waWMucG5nIiwiaXAiOiIxMjcuMC4wLjEiLCJlbWFpbCI6Im1lQGV4YW1wbGUuY29tIn19.qsR_PupfjIq7uw0eAuyGV8nsUoMx9v541c9olnRInRQ`
|
||||
|
||||
var anonToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImFub255bW91cyB0ZXN0IHVzZXIiLCJpZCI6ImFub255bW91c190ZXN0X3VzZXIiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoiYW5vbkBleGFtcGxlLmNvbSJ9fQ.gAae2WMxZNZE5ebVboptPEyQ7Nk6EQxciNnGJ_mPOuU`
|
||||
|
||||
var devTokenBadAud = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0Ml9iYWQiLCJleHAiOjM3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTIxODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJkZXZlbG9wZXIgb25lIiwiaWQiOiJkZXYiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20ifX0.FuTTocVtcxr4VjpfIICvU2yOb3su28VkDzj94H9Q3xY`
|
||||
var emailUserToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6Imdvb2RAZXhhbXBsZS5jb20gdGVzdCB1c2VyIiwiaWQiOiJlbWFpbF9mNWRmZTlkMmU2YmQ3NWZjNzRlYTVmYWJmMjczYjQ1YjViYWViMTk1IiwicGljdHVyZSI6Imh0dHA6Ly9leGFtcGxlLmNvbS9waWMucG5nIiwiaXAiOiIxMjcuMC4wLjEiLCJlbWFpbCI6Imdvb2RAZXhhbXBsZS5jb20ifX0.vH2HN1JpuXL8okTJq1A-zGHQ-l2ILcwxvDDEmu2zwks`
|
||||
|
||||
var devTokenBadAud = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0Ml9iYWQiLCJleHAiOjM3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTIxODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJkZXZlbG9wZXIgb25lIiwiaWQiOiJwcm92aWRlcjFfZGV2IiwicGljdHVyZSI6Imh0dHA6Ly9leGFtcGxlLmNvbS9waWMucG5nIiwiaXAiOiIxMjcuMC4wLjEiLCJlbWFpbCI6Im1lQGV4YW1wbGUuY29tIn19.X-lvnHvBz6VfEbVV4f-bjcZuLY5pYtvEansk_TQMrX8`
|
||||
|
||||
var adminUmputunToken = `eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTk1NDU5Nzk4MCwianRpIjoiOTdhMmUwYWM0ZGM3ZDVmNjkyNmQ1ZTg2MjBhY2VmOWE0MGMwIiwiaWF0IjoxNDU0NTk3NjgwLCJpc3MiOiJyZW1hcms0MiIsInVzZXIiOnsibmFtZSI6IlVtcHV0dW4iLCJpZCI6ImdpdGh1Yl9lZjBmNzA2YTciLCJwaWN0dXJlIjoiaHR0cHM6Ly9yZW1hcms0Mi5yYWRpby10LmNvbS9hcGkvdjEvYXZhdGFyL2NiNDJmZjQ5M2FkZTY5NmQ4OGEzYTU5MGYxMzZhZTllMzRkZTdjMWIuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.dZiOjWHguo9f42XCMooMcv4EmYFzifl_-LEvPZHCtks`
|
||||
|
||||
@@ -60,23 +68,6 @@ func TestRest_FileServer(t *testing.T) {
|
||||
_ = os.Remove(testHTMLFile)
|
||||
}
|
||||
|
||||
func TestRest_GetStarted(t *testing.T) {
|
||||
ts, _, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
getStartedHTML := os.TempDir() + "/getstarted.html"
|
||||
err := os.WriteFile(getStartedHTML, []byte("some html blah"), 0o700)
|
||||
assert.NoError(t, err)
|
||||
|
||||
body, code := get(t, ts.URL+"/index.html")
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
assert.Equal(t, "some html blah", body)
|
||||
|
||||
_ = os.Remove(getStartedHTML)
|
||||
_, code = get(t, ts.URL+"/index.html")
|
||||
assert.Equal(t, http.StatusNotFound, code)
|
||||
}
|
||||
|
||||
func TestRest_Shutdown(t *testing.T) {
|
||||
srv := Rest{Authenticator: &auth.Service{}, ImageProxy: &proxy.Image{}}
|
||||
done := make(chan bool)
|
||||
@@ -313,7 +304,7 @@ func TestRest_cacheControl(t *testing.T) {
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tt.url, nil)
|
||||
req := httptest.NewRequest("GET", tt.url, http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h := cacheControl(tt.exp, tt.version)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
@@ -341,7 +332,7 @@ func TestRest_frameAncestors(t *testing.T) {
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||
req := httptest.NewRequest("GET", "http://example.com", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h := frameAncestors(tt.hosts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
@@ -375,7 +366,7 @@ func TestRest_subscribersOnly(t *testing.T) {
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||
req := httptest.NewRequest("GET", "http://example.com", http.NoBody)
|
||||
if tt.setUser {
|
||||
req = token.SetUserInfo(req, tt.user)
|
||||
}
|
||||
@@ -389,6 +380,38 @@ func TestRest_subscribersOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validEmailAuth(t *testing.T) {
|
||||
tbl := []struct {
|
||||
req string
|
||||
status int
|
||||
}{
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone", http.StatusOK},
|
||||
{"/auth/email/login?site=site-with-dash_and_underscore-and.dot&address=umputun%example.com&user=someone", http.StatusOK},
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone+blah", http.StatusOK},
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=Евгений+Умпутун", http.StatusOK},
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=12", http.StatusForbidden},
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=..blah+blah", http.StatusForbidden},
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someonelooong+loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong", http.StatusForbidden},
|
||||
{"/auth/twitter/login?site=remark42&address=umputun%example.com&user=..blah+blah", http.StatusOK},
|
||||
{"/auth/email/login?site=remark42&address=umputun%example.com", http.StatusOK},
|
||||
{"/auth/email/login?site=remark42&address=umputun+example.com&user=someone", http.StatusForbidden},
|
||||
{"/auth/email/login?site=bad!site&address=umputun%example.com&user=someone", http.StatusForbidden},
|
||||
{"/auth/email/login?site=loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongsite&address=umputun%example.com&user=someone", http.StatusForbidden},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com"+tt.req, http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
h := validEmailAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
h.ServeHTTP(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, tt.status, resp.StatusCode)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// randomPath pick a file or folder name which is not in use for sure
|
||||
func randomPath(tempDir, basename, suffix string) (string, error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
@@ -469,6 +492,14 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
|
||||
}
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = -5, -10
|
||||
|
||||
// add some providers. Needed because we don't allow users with unlisted providers to authenticate
|
||||
providers := []string{"provider1", "anonymous", "github", "email"}
|
||||
for _, p := range providers {
|
||||
srv.Authenticator.AddDirectProvider(p, provider.CredCheckerFunc(func(user, password string) (ok bool, err error) {
|
||||
return true, nil
|
||||
}))
|
||||
}
|
||||
|
||||
for _, h := range srvHook {
|
||||
h(srv)
|
||||
}
|
||||
@@ -532,6 +563,20 @@ func getWithDevAuth(t *testing.T, url string) (body string, code int) {
|
||||
return string(b), r.StatusCode
|
||||
}
|
||||
|
||||
func getWithDev2Auth(t *testing.T, url string) (body string, code int) {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
defer client.CloseIdleConnections()
|
||||
req, err := http.NewRequest("GET", url, http.NoBody)
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("X-JWT", dev2Token)
|
||||
r, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
b, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, r.Body.Close())
|
||||
return string(b), r.StatusCode
|
||||
}
|
||||
|
||||
func getWithAdminAuth(t *testing.T, url string) (response string, statusCode int) {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
@@ -111,15 +111,11 @@ func (s *Rest) getRemarkHost() string {
|
||||
|
||||
func (s *Rest) makeTLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
// tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CurvePreferences: []tls.CurveID{
|
||||
|
||||
@@ -48,9 +48,9 @@ type errTmplData struct {
|
||||
Details string
|
||||
}
|
||||
|
||||
// SendErrorHTML makes html body with provided template and responds with provided http status code,
|
||||
// SendErrorHTML makes html body from error_response.html.tmpl template and responds with provided http status code,
|
||||
// error code is not included in render as it is intended for UI developers and not for the users
|
||||
func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int, t templates.FileReader) {
|
||||
func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int) {
|
||||
// MustExecute behaves like template.Execute, but panics if an error occurs.
|
||||
MustExecute := func(tmpl *template.Template, wr io.Writer, data interface{}) {
|
||||
if err = tmpl.Execute(wr, data); err != nil {
|
||||
@@ -58,7 +58,7 @@ func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, e
|
||||
}
|
||||
}
|
||||
MustRead := func(path string) string {
|
||||
file, e := t.ReadFile(path)
|
||||
file, e := templates.Read(path)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
@@ -36,18 +36,11 @@ func TestSendErrorJSON(t *testing.T) {
|
||||
assert.Equal(t, `{"code":123,"details":"error details 123456","error":"error 500"}`+"\n", string(body))
|
||||
}
|
||||
|
||||
type MockFS struct{}
|
||||
|
||||
func (fs *MockFS) ReadFile(path string) ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("{{.Error}}{{.Details}} %s", path)), nil
|
||||
}
|
||||
|
||||
func TestSendErrorHTML(t *testing.T) {
|
||||
fs := &MockFS{}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/error" {
|
||||
t.Log("http err request", r.URL)
|
||||
SendErrorHTML(w, r, 500, fmt.Errorf("error 500"), "error details 123456", 987, fs)
|
||||
SendErrorHTML(w, r, 500, fmt.Errorf("error 500"), "error details 123456", 987)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(404)
|
||||
|
||||
@@ -159,7 +159,7 @@ func (p Image) downloadImage(ctx context.Context, imgURL string) ([]byte, error)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't download image %s: %w", imgURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer resp.Body.Close() //nolint gosec // we don't care about response body
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("got unsuccessful response status %d while fetching %s", resp.StatusCode, imgURL)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/remark42/backend/app/store/image"
|
||||
@@ -95,7 +94,8 @@ func TestImage_Replace(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImage_Routes(t *testing.T) {
|
||||
imageStore := image.MockStore{}
|
||||
// no image supposed to be cached
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||
img := Image{
|
||||
HTTP2HTTPS: true,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
@@ -110,9 +110,6 @@ func TestImage_Routes(t *testing.T) {
|
||||
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||
|
||||
// no image supposed to be cached
|
||||
imageStore.On("Load", mock.Anything).Times(2).Return(nil, nil)
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
@@ -131,10 +128,11 @@ func TestImage_Routes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
assert.Equal(t, 2, len(imageStore.LoadCalls()))
|
||||
}
|
||||
|
||||
func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
||||
imageStore := image.MockStore{}
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||
img := Image{
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
RoutePath: "/api/v1/proxy",
|
||||
@@ -148,8 +146,6 @@ func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
||||
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||
|
||||
imageStore.On("Load", mock.Anything).Once().Return(nil, nil)
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
@@ -157,11 +153,18 @@ func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
||||
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
||||
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
||||
|
||||
imageStore.AssertCalled(t, "Load", mock.Anything)
|
||||
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||
}
|
||||
|
||||
func TestImage_RoutesCachingImage(t *testing.T) {
|
||||
imageStore := image.MockStore{}
|
||||
imageStore := image.StoreMock{
|
||||
LoadFunc: func(id string) ([]byte, error) {
|
||||
return nil, nil
|
||||
},
|
||||
SaveFunc: func(id string, img []byte) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
img := Image{
|
||||
CacheExternal: true,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
@@ -177,9 +180,6 @@ func TestImage_RoutesCachingImage(t *testing.T) {
|
||||
imgURL := httpSrv.URL + "/image/img1.png"
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(imgURL))
|
||||
|
||||
imageStore.On("Load", mock.Anything).Once().Return(nil, nil)
|
||||
imageStore.On("Save", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
@@ -187,12 +187,18 @@ func TestImage_RoutesCachingImage(t *testing.T) {
|
||||
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
||||
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
||||
|
||||
imageStore.AssertCalled(t, "Load", mock.Anything)
|
||||
imageStore.AssertCalled(t, "Save", "cached_images/4b84b15bff6ee5796152495a230e45e3d7e947d9-"+image.Sha1Str(imgURL), gopherPNGBytes())
|
||||
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||
assert.Equal(t, 1, len(imageStore.SaveCalls()))
|
||||
assert.Equal(t, "cached_images/4b84b15bff6ee5796152495a230e45e3d7e947d9-"+image.Sha1Str(imgURL), imageStore.SaveCalls()[0].ID)
|
||||
assert.Equal(t, gopherPNGBytes(), imageStore.SaveCalls()[0].Img)
|
||||
}
|
||||
|
||||
func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||
imageStore := image.MockStore{}
|
||||
// In order to validate that cached data used cache "will return" some other data from what http server would
|
||||
testImage := []byte(fmt.Sprintf("%256s", "X"))
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) {
|
||||
return testImage, nil
|
||||
}}
|
||||
img := Image{
|
||||
CacheExternal: true,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
@@ -207,10 +213,6 @@ func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||
|
||||
// In order to validate that cached data used cache "will return" some other data from what http server would
|
||||
testImage := []byte(fmt.Sprintf("%256s", "X"))
|
||||
imageStore.On("Load", mock.Anything).Once().Return(testImage, nil)
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, resp.Body.Close())
|
||||
@@ -219,11 +221,12 @@ func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header["Content-Type"][0],
|
||||
"if you save text you receive text/plain in response, that's only fair option you got")
|
||||
|
||||
imageStore.AssertCalled(t, "Load", mock.Anything)
|
||||
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||
}
|
||||
|
||||
func TestImage_RoutesTimedOut(t *testing.T) {
|
||||
imageStore := image.MockStore{}
|
||||
// no image supposed to be cached
|
||||
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||
img := Image{
|
||||
HTTP2HTTPS: true,
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
@@ -239,9 +242,6 @@ func TestImage_RoutesTimedOut(t *testing.T) {
|
||||
|
||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img-slow.png"))
|
||||
|
||||
// no image supposed to be cached
|
||||
imageStore.On("Load", mock.Anything).Once().Return(nil, nil)
|
||||
|
||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
@@ -250,6 +250,7 @@ func TestImage_RoutesTimedOut(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
t.Log(string(b))
|
||||
assert.True(t, strings.Contains(string(b), "deadline exceeded"))
|
||||
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||
}
|
||||
|
||||
func TestImage_ConvertProxyMode(t *testing.T) {
|
||||
|
||||
@@ -78,5 +78,5 @@ func (s *StaticStore) Enabled(site string) (ok bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// OnEvent doesn nothing for StaticStore
|
||||
// OnEvent does nothing for StaticStore
|
||||
func (s *StaticStore) OnEvent(_ string, _ EventType) error { return nil }
|
||||
|
||||
@@ -15,7 +15,7 @@ type Comment struct {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
ParentID string `json:"pid"`
|
||||
Text string `json:"text"`
|
||||
Orig string `json:"orig,omitempty"`
|
||||
Orig string `json:"orig,omitempty"` // important: never render this as HTML! It's not sanitized.
|
||||
User User `json:"user"`
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"`
|
||||
@@ -112,20 +112,23 @@ func (c *Comment) SetDeleted(mode DeleteMode) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize clean dangerous html/js from the comment
|
||||
// Sanitize clean dangerous html/js from the comment.
|
||||
// Comment.Orig which is used to store the original comment text is not sanitized
|
||||
// as we expect to never render it as HTML and render Comment.Text instead
|
||||
func (c *Comment) Sanitize() {
|
||||
p := bluemonday.UGCPolicy()
|
||||
p.AllowAttrs("class").Matching(regexp.MustCompile("^chroma$")).OnElements("pre")
|
||||
// special case for embedding the quotes from Twitter
|
||||
p.AllowAttrs("class").Matching(regexp.MustCompile("^twitter-tweet$")).OnElements("blockquote")
|
||||
// this is list of <span> tag classes which could be produced by chroma code renderer
|
||||
// source: https://github.com/alecthomas/chroma/blob/022b6f4fc2c4aa819aac18363c8de3f70619200b/types.go#L221-L316
|
||||
const codeSpanClassRegex = "^(chroma|ln|lnt|hl|lntable|lntd|w|err|x|esc|k|kc" +
|
||||
// source: https://github.com/alecthomas/chroma/blob/c263f6f/types.go#L209-L306
|
||||
const codeSpanClassRegex = "^(bg|chroma|line|ln|lnt|hl|lntable|lntd|lnlinks|cl|w|err|x|k|kc" +
|
||||
"|kd|kn|kp|kr|kt|n|na|nb|bp|nc|no|nd|ni|ne|nf|fm|py|nl|nn|nx|nt|nv|vc|vg" +
|
||||
"|vi|vm|l|ld|s|sa|sb|sc|dl|sd|s2|se|sh|si|sx|sr|s1|ss|m|mb|mf|mh|mi|il" +
|
||||
"|mo|o|ow|p|c|ch|cm|cp|cpf|c1|cs|g|gd|ge|gr|gh|gi|go|gp|gs|gu|gt|gl)$"
|
||||
p.AllowAttrs("class").Matching(regexp.MustCompile(codeSpanClassRegex)).OnElements("span")
|
||||
p.AllowAttrs("loading").Matching(regexp.MustCompile("^(lazy|eager)$")).OnElements("img")
|
||||
c.Text = p.Sanitize(c.Text)
|
||||
c.Orig = p.Sanitize(c.Orig)
|
||||
c.User.ID = template.HTMLEscapeString(c.User.ID)
|
||||
c.User.Name = c.SanitizeText(c.User.Name)
|
||||
c.User.Picture = c.SanitizeAsURL(c.User.Picture)
|
||||
@@ -143,7 +146,7 @@ func (c *Comment) Snippet(limit int) string {
|
||||
if size < limit {
|
||||
return cleanText
|
||||
}
|
||||
snippet := []rune(cleanText)[:size]
|
||||
snippet := []rune(cleanText)[:limit]
|
||||
// go back in snippet and found the first space
|
||||
for i := len(snippet) - 1; i >= 0; i-- {
|
||||
if snippet[i] == ' ' {
|
||||
@@ -151,6 +154,10 @@ func (c *Comment) Snippet(limit int) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Don't add a space if comment is just a one single word which has been truncated.
|
||||
if len(snippet) == limit {
|
||||
return string(snippet) + "..."
|
||||
}
|
||||
return string(snippet) + " ..."
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,10 @@ func TestComment_Sanitize(t *testing.T) {
|
||||
inp: Comment{Text: "blah blah", PostTitle: "<script>alert()</script>something"},
|
||||
out: Comment{Text: "blah blah", PostTitle: "something"},
|
||||
},
|
||||
{
|
||||
inp: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw">@marcsheep</a> <a href="https://t.co/ULnVpG8w55">pic.twitter.com/ULnVpG8w55</a></p>— Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw">November 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, PostTitle: "Twitter quote"},
|
||||
out: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn" rel="nofollow">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw" rel="nofollow">@marcsheep</a> <a href="https://t.co/ULnVpG8w55" rel="nofollow">pic.twitter.com/ULnVpG8w55</a></p>— Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw" rel="nofollow">November 9, 2020</a></blockquote> `, PostTitle: "Twitter quote"},
|
||||
},
|
||||
}
|
||||
|
||||
for n, tt := range tbl {
|
||||
@@ -192,8 +196,8 @@ func TestComment_Snippet(t *testing.T) {
|
||||
{0, "", ""},
|
||||
{-1, "test\nblah", "test blah"},
|
||||
{5, "test\nblah", "test ..."},
|
||||
{5, "xyz12345 xxx", "xyz12345 ..."},
|
||||
{10, "xyz12345 xxx\ntest 123456", "xyz12345 xxx test ..."},
|
||||
{5, "xyz12345 xxx", "xyz12..."},
|
||||
{10, "xyz12345 xxx\ntest 123456", "xyz12345 ..."},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
@@ -259,7 +263,7 @@ func TestComment_sanitizeText(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"<a href=javascript:alert(document.domain)//>xxx</a>",
|
||||
"<a/>xxx</a>",
|
||||
"xxx</a>",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@ import (
|
||||
|
||||
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
|
||||
// there are 6 types of top-level buckets:
|
||||
// - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment
|
||||
// - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID
|
||||
// value is not full comment but a reference combined from post-url+commentID
|
||||
// - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value
|
||||
// is a nested bucket named userID with kv as ts:reference
|
||||
// - users details in "user_details" bucket. Key is userID, value - UserDetailEntry
|
||||
// - blocking info sits in "block" bucket. Key is userID, value - ts
|
||||
// - counts per post to keep number of comments. Key is post url, value - count
|
||||
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
|
||||
// - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment
|
||||
// - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID
|
||||
// value is not full comment but a reference combined from post-url+commentID
|
||||
// - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value
|
||||
// is a nested bucket named userID with kv as ts:reference
|
||||
// - users details in "user_details" bucket. Key is userID, value - UserDetailEntry
|
||||
// - blocking info sits in "block" bucket. Key is userID, value - ts
|
||||
// - counts per post to keep number of comments. Key is post url, value - count
|
||||
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
|
||||
type BoltDB struct {
|
||||
dbs map[string]*bolt.DB
|
||||
}
|
||||
@@ -780,7 +780,7 @@ func (b *BoltDB) deleteUserDetail(bdb *bolt.DB, userID string, userDetail UserDe
|
||||
}
|
||||
|
||||
return bdb.Update(func(tx *bolt.Tx) error {
|
||||
// updated entry is not empty and we need to store it's updated copy
|
||||
// updated entry is not empty and we need to store its updated copy
|
||||
err := b.save(tx.Bucket([]byte(userDetailsBucketName)), userID, entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update detail %s for %s: %w", userDetail, userID, err)
|
||||
|
||||
@@ -911,6 +911,13 @@ func TestBoltDB_NewFailed(t *testing.T) {
|
||||
assert.EqualError(t, err, "failed to make boltdb for /tmp/no-such-place/tmp.db: open /tmp/no-such-place/tmp.db: no such file or directory")
|
||||
}
|
||||
|
||||
func TestBoltDB_DoubleClose(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
assert.NoError(t, b.Close())
|
||||
assert.NoError(t, b.Close(), "second call should not result in panic or errors")
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
func prep(t *testing.T) (b *BoltDB, teardown func()) {
|
||||
_ = os.Remove(testDB)
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/umputun/remark42/backend/app/store"
|
||||
)
|
||||
|
||||
// NOTE: mockery works from linked to go-path and with GOFLAGS='-mod=vendor' go generate
|
||||
//go:generate sh -c "mockery -inpkg -name Interface -print > /tmp/engine-mock.tmp && mv /tmp/engine-mock.tmp engine_mock.go"
|
||||
// NOTE: matryer/moq should be installed globally and works with `go generate ./...`
|
||||
//go:generate moq --out engine_mock.go . Interface
|
||||
|
||||
// Interface defines methods provided by low-level storage engine
|
||||
type Interface interface {
|
||||
|
||||
@@ -1,231 +1,508 @@
|
||||
// Code generated by mockery v1.1.2. DO NOT EDIT.
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
store "github.com/umputun/remark42/backend/app/store"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MockInterface is an autogenerated mock type for the Interface type
|
||||
type MockInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
// Ensure, that InterfaceMock does implement Interface.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ Interface = &InterfaceMock{}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *MockInterface) Close() error {
|
||||
ret := _m.Called()
|
||||
// InterfaceMock is a mock implementation of Interface.
|
||||
//
|
||||
// func TestSomethingThatUsesInterface(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked Interface
|
||||
// mockedInterface := &InterfaceMock{
|
||||
// CloseFunc: func() error {
|
||||
// panic("mock out the Close method")
|
||||
// },
|
||||
// CountFunc: func(req FindRequest) (int, error) {
|
||||
// panic("mock out the Count method")
|
||||
// },
|
||||
// CreateFunc: func(comment store.Comment) (string, error) {
|
||||
// panic("mock out the Create method")
|
||||
// },
|
||||
// DeleteFunc: func(req DeleteRequest) error {
|
||||
// panic("mock out the Delete method")
|
||||
// },
|
||||
// FindFunc: func(req FindRequest) ([]store.Comment, error) {
|
||||
// panic("mock out the Find method")
|
||||
// },
|
||||
// FlagFunc: func(req FlagRequest) (bool, error) {
|
||||
// panic("mock out the Flag method")
|
||||
// },
|
||||
// GetFunc: func(req GetRequest) (store.Comment, error) {
|
||||
// panic("mock out the Get method")
|
||||
// },
|
||||
// InfoFunc: func(req InfoRequest) ([]store.PostInfo, error) {
|
||||
// panic("mock out the Info method")
|
||||
// },
|
||||
// ListFlagsFunc: func(req FlagRequest) ([]interface{}, error) {
|
||||
// panic("mock out the ListFlags method")
|
||||
// },
|
||||
// UpdateFunc: func(comment store.Comment) error {
|
||||
// panic("mock out the Update method")
|
||||
// },
|
||||
// UserDetailFunc: func(req UserDetailRequest) ([]UserDetailEntry, error) {
|
||||
// panic("mock out the UserDetail method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedInterface in code that requires Interface
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type InterfaceMock struct {
|
||||
// CloseFunc mocks the Close method.
|
||||
CloseFunc func() error
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
// CountFunc mocks the Count method.
|
||||
CountFunc func(req FindRequest) (int, error)
|
||||
|
||||
return r0
|
||||
}
|
||||
// CreateFunc mocks the Create method.
|
||||
CreateFunc func(comment store.Comment) (string, error)
|
||||
|
||||
// Count provides a mock function with given fields: req
|
||||
func (_m *MockInterface) Count(req FindRequest) (int, error) {
|
||||
ret := _m.Called(req)
|
||||
// DeleteFunc mocks the Delete method.
|
||||
DeleteFunc func(req DeleteRequest) error
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(FindRequest) int); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
// FindFunc mocks the Find method.
|
||||
FindFunc func(req FindRequest) ([]store.Comment, error)
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(FindRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
// FlagFunc mocks the Flag method.
|
||||
FlagFunc func(req FlagRequest) (bool, error)
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
// GetFunc mocks the Get method.
|
||||
GetFunc func(req GetRequest) (store.Comment, error)
|
||||
|
||||
// Create provides a mock function with given fields: comment
|
||||
func (_m *MockInterface) Create(comment store.Comment) (string, error) {
|
||||
ret := _m.Called(comment)
|
||||
// InfoFunc mocks the Info method.
|
||||
InfoFunc func(req InfoRequest) ([]store.PostInfo, error)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(store.Comment) string); ok {
|
||||
r0 = rf(comment)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
// ListFlagsFunc mocks the ListFlags method.
|
||||
ListFlagsFunc func(req FlagRequest) ([]interface{}, error)
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(store.Comment) error); ok {
|
||||
r1 = rf(comment)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
// UpdateFunc mocks the Update method.
|
||||
UpdateFunc func(comment store.Comment) error
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
// UserDetailFunc mocks the UserDetail method.
|
||||
UserDetailFunc func(req UserDetailRequest) ([]UserDetailEntry, error)
|
||||
|
||||
// Delete provides a mock function with given fields: req
|
||||
func (_m *MockInterface) Delete(req DeleteRequest) error {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(DeleteRequest) error); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Find provides a mock function with given fields: req
|
||||
func (_m *MockInterface) Find(req FindRequest) ([]store.Comment, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 []store.Comment
|
||||
if rf, ok := ret.Get(0).(func(FindRequest) []store.Comment); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]store.Comment)
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// Close holds details about calls to the Close method.
|
||||
Close []struct {
|
||||
}
|
||||
// Count holds details about calls to the Count method.
|
||||
Count []struct {
|
||||
// Req is the req argument value.
|
||||
Req FindRequest
|
||||
}
|
||||
// Create holds details about calls to the Create method.
|
||||
Create []struct {
|
||||
// Comment is the comment argument value.
|
||||
Comment store.Comment
|
||||
}
|
||||
// Delete holds details about calls to the Delete method.
|
||||
Delete []struct {
|
||||
// Req is the req argument value.
|
||||
Req DeleteRequest
|
||||
}
|
||||
// Find holds details about calls to the Find method.
|
||||
Find []struct {
|
||||
// Req is the req argument value.
|
||||
Req FindRequest
|
||||
}
|
||||
// Flag holds details about calls to the Flag method.
|
||||
Flag []struct {
|
||||
// Req is the req argument value.
|
||||
Req FlagRequest
|
||||
}
|
||||
// Get holds details about calls to the Get method.
|
||||
Get []struct {
|
||||
// Req is the req argument value.
|
||||
Req GetRequest
|
||||
}
|
||||
// Info holds details about calls to the Info method.
|
||||
Info []struct {
|
||||
// Req is the req argument value.
|
||||
Req InfoRequest
|
||||
}
|
||||
// ListFlags holds details about calls to the ListFlags method.
|
||||
ListFlags []struct {
|
||||
// Req is the req argument value.
|
||||
Req FlagRequest
|
||||
}
|
||||
// Update holds details about calls to the Update method.
|
||||
Update []struct {
|
||||
// Comment is the comment argument value.
|
||||
Comment store.Comment
|
||||
}
|
||||
// UserDetail holds details about calls to the UserDetail method.
|
||||
UserDetail []struct {
|
||||
// Req is the req argument value.
|
||||
Req UserDetailRequest
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(FindRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
lockClose sync.RWMutex
|
||||
lockCount sync.RWMutex
|
||||
lockCreate sync.RWMutex
|
||||
lockDelete sync.RWMutex
|
||||
lockFind sync.RWMutex
|
||||
lockFlag sync.RWMutex
|
||||
lockGet sync.RWMutex
|
||||
lockInfo sync.RWMutex
|
||||
lockListFlags sync.RWMutex
|
||||
lockUpdate sync.RWMutex
|
||||
lockUserDetail sync.RWMutex
|
||||
}
|
||||
|
||||
// Flag provides a mock function with given fields: req
|
||||
func (_m *MockInterface) Flag(req FlagRequest) (bool, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(FlagRequest) bool); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
// Close calls CloseFunc.
|
||||
func (mock *InterfaceMock) Close() error {
|
||||
if mock.CloseFunc == nil {
|
||||
panic("InterfaceMock.CloseFunc: method is nil but Interface.Close was just called")
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(FlagRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockClose.Lock()
|
||||
mock.calls.Close = append(mock.calls.Close, callInfo)
|
||||
mock.lockClose.Unlock()
|
||||
return mock.CloseFunc()
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: req
|
||||
func (_m *MockInterface) Get(req GetRequest) (store.Comment, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 store.Comment
|
||||
if rf, ok := ret.Get(0).(func(GetRequest) store.Comment); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
r0 = ret.Get(0).(store.Comment)
|
||||
// CloseCalls gets all the calls that were made to Close.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.CloseCalls())
|
||||
func (mock *InterfaceMock) CloseCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(GetRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
mock.lockClose.RLock()
|
||||
calls = mock.calls.Close
|
||||
mock.lockClose.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Info provides a mock function with given fields: req
|
||||
func (_m *MockInterface) Info(req InfoRequest) ([]store.PostInfo, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 []store.PostInfo
|
||||
if rf, ok := ret.Get(0).(func(InfoRequest) []store.PostInfo); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]store.PostInfo)
|
||||
}
|
||||
// Count calls CountFunc.
|
||||
func (mock *InterfaceMock) Count(req FindRequest) (int, error) {
|
||||
if mock.CountFunc == nil {
|
||||
panic("InterfaceMock.CountFunc: method is nil but Interface.Count was just called")
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(InfoRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
callInfo := struct {
|
||||
Req FindRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
mock.lockCount.Lock()
|
||||
mock.calls.Count = append(mock.calls.Count, callInfo)
|
||||
mock.lockCount.Unlock()
|
||||
return mock.CountFunc(req)
|
||||
}
|
||||
|
||||
// ListFlags provides a mock function with given fields: req
|
||||
func (_m *MockInterface) ListFlags(req FlagRequest) ([]interface{}, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 []interface{}
|
||||
if rf, ok := ret.Get(0).(func(FlagRequest) []interface{}); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]interface{})
|
||||
}
|
||||
// CountCalls gets all the calls that were made to Count.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.CountCalls())
|
||||
func (mock *InterfaceMock) CountCalls() []struct {
|
||||
Req FindRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req FindRequest
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(FlagRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
mock.lockCount.RLock()
|
||||
calls = mock.calls.Count
|
||||
mock.lockCount.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: comment
|
||||
func (_m *MockInterface) Update(comment store.Comment) error {
|
||||
ret := _m.Called(comment)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(store.Comment) error); ok {
|
||||
r0 = rf(comment)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
// Create calls CreateFunc.
|
||||
func (mock *InterfaceMock) Create(comment store.Comment) (string, error) {
|
||||
if mock.CreateFunc == nil {
|
||||
panic("InterfaceMock.CreateFunc: method is nil but Interface.Create was just called")
|
||||
}
|
||||
|
||||
return r0
|
||||
callInfo := struct {
|
||||
Comment store.Comment
|
||||
}{
|
||||
Comment: comment,
|
||||
}
|
||||
mock.lockCreate.Lock()
|
||||
mock.calls.Create = append(mock.calls.Create, callInfo)
|
||||
mock.lockCreate.Unlock()
|
||||
return mock.CreateFunc(comment)
|
||||
}
|
||||
|
||||
// UserDetail provides a mock function with given fields: req
|
||||
func (_m *MockInterface) UserDetail(req UserDetailRequest) ([]UserDetailEntry, error) {
|
||||
ret := _m.Called(req)
|
||||
|
||||
var r0 []UserDetailEntry
|
||||
if rf, ok := ret.Get(0).(func(UserDetailRequest) []UserDetailEntry); ok {
|
||||
r0 = rf(req)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]UserDetailEntry)
|
||||
}
|
||||
// CreateCalls gets all the calls that were made to Create.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.CreateCalls())
|
||||
func (mock *InterfaceMock) CreateCalls() []struct {
|
||||
Comment store.Comment
|
||||
} {
|
||||
var calls []struct {
|
||||
Comment store.Comment
|
||||
}
|
||||
mock.lockCreate.RLock()
|
||||
calls = mock.calls.Create
|
||||
mock.lockCreate.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(UserDetailRequest) error); ok {
|
||||
r1 = rf(req)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
// Delete calls DeleteFunc.
|
||||
func (mock *InterfaceMock) Delete(req DeleteRequest) error {
|
||||
if mock.DeleteFunc == nil {
|
||||
panic("InterfaceMock.DeleteFunc: method is nil but Interface.Delete was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Req DeleteRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
mock.lockDelete.Lock()
|
||||
mock.calls.Delete = append(mock.calls.Delete, callInfo)
|
||||
mock.lockDelete.Unlock()
|
||||
return mock.DeleteFunc(req)
|
||||
}
|
||||
|
||||
// DeleteCalls gets all the calls that were made to Delete.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.DeleteCalls())
|
||||
func (mock *InterfaceMock) DeleteCalls() []struct {
|
||||
Req DeleteRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req DeleteRequest
|
||||
}
|
||||
mock.lockDelete.RLock()
|
||||
calls = mock.calls.Delete
|
||||
mock.lockDelete.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Find calls FindFunc.
|
||||
func (mock *InterfaceMock) Find(req FindRequest) ([]store.Comment, error) {
|
||||
if mock.FindFunc == nil {
|
||||
panic("InterfaceMock.FindFunc: method is nil but Interface.Find was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Req FindRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
mock.lockFind.Lock()
|
||||
mock.calls.Find = append(mock.calls.Find, callInfo)
|
||||
mock.lockFind.Unlock()
|
||||
return mock.FindFunc(req)
|
||||
}
|
||||
|
||||
// FindCalls gets all the calls that were made to Find.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.FindCalls())
|
||||
func (mock *InterfaceMock) FindCalls() []struct {
|
||||
Req FindRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req FindRequest
|
||||
}
|
||||
mock.lockFind.RLock()
|
||||
calls = mock.calls.Find
|
||||
mock.lockFind.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
// Flag calls FlagFunc.
|
||||
func (mock *InterfaceMock) Flag(req FlagRequest) (bool, error) {
|
||||
if mock.FlagFunc == nil {
|
||||
panic("InterfaceMock.FlagFunc: method is nil but Interface.Flag was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Req FlagRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
mock.lockFlag.Lock()
|
||||
mock.calls.Flag = append(mock.calls.Flag, callInfo)
|
||||
mock.lockFlag.Unlock()
|
||||
return mock.FlagFunc(req)
|
||||
}
|
||||
|
||||
// FlagCalls gets all the calls that were made to Flag.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.FlagCalls())
|
||||
func (mock *InterfaceMock) FlagCalls() []struct {
|
||||
Req FlagRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req FlagRequest
|
||||
}
|
||||
mock.lockFlag.RLock()
|
||||
calls = mock.calls.Flag
|
||||
mock.lockFlag.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Get calls GetFunc.
|
||||
func (mock *InterfaceMock) Get(req GetRequest) (store.Comment, error) {
|
||||
if mock.GetFunc == nil {
|
||||
panic("InterfaceMock.GetFunc: method is nil but Interface.Get was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Req GetRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
mock.lockGet.Lock()
|
||||
mock.calls.Get = append(mock.calls.Get, callInfo)
|
||||
mock.lockGet.Unlock()
|
||||
return mock.GetFunc(req)
|
||||
}
|
||||
|
||||
// GetCalls gets all the calls that were made to Get.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.GetCalls())
|
||||
func (mock *InterfaceMock) GetCalls() []struct {
|
||||
Req GetRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req GetRequest
|
||||
}
|
||||
mock.lockGet.RLock()
|
||||
calls = mock.calls.Get
|
||||
mock.lockGet.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Info calls InfoFunc.
|
||||
func (mock *InterfaceMock) Info(req InfoRequest) ([]store.PostInfo, error) {
|
||||
if mock.InfoFunc == nil {
|
||||
panic("InterfaceMock.InfoFunc: method is nil but Interface.Info was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Req InfoRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
mock.lockInfo.Lock()
|
||||
mock.calls.Info = append(mock.calls.Info, callInfo)
|
||||
mock.lockInfo.Unlock()
|
||||
return mock.InfoFunc(req)
|
||||
}
|
||||
|
||||
// InfoCalls gets all the calls that were made to Info.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.InfoCalls())
|
||||
func (mock *InterfaceMock) InfoCalls() []struct {
|
||||
Req InfoRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req InfoRequest
|
||||
}
|
||||
mock.lockInfo.RLock()
|
||||
calls = mock.calls.Info
|
||||
mock.lockInfo.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ListFlags calls ListFlagsFunc.
|
||||
func (mock *InterfaceMock) ListFlags(req FlagRequest) ([]interface{}, error) {
|
||||
if mock.ListFlagsFunc == nil {
|
||||
panic("InterfaceMock.ListFlagsFunc: method is nil but Interface.ListFlags was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Req FlagRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
mock.lockListFlags.Lock()
|
||||
mock.calls.ListFlags = append(mock.calls.ListFlags, callInfo)
|
||||
mock.lockListFlags.Unlock()
|
||||
return mock.ListFlagsFunc(req)
|
||||
}
|
||||
|
||||
// ListFlagsCalls gets all the calls that were made to ListFlags.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.ListFlagsCalls())
|
||||
func (mock *InterfaceMock) ListFlagsCalls() []struct {
|
||||
Req FlagRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req FlagRequest
|
||||
}
|
||||
mock.lockListFlags.RLock()
|
||||
calls = mock.calls.ListFlags
|
||||
mock.lockListFlags.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Update calls UpdateFunc.
|
||||
func (mock *InterfaceMock) Update(comment store.Comment) error {
|
||||
if mock.UpdateFunc == nil {
|
||||
panic("InterfaceMock.UpdateFunc: method is nil but Interface.Update was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Comment store.Comment
|
||||
}{
|
||||
Comment: comment,
|
||||
}
|
||||
mock.lockUpdate.Lock()
|
||||
mock.calls.Update = append(mock.calls.Update, callInfo)
|
||||
mock.lockUpdate.Unlock()
|
||||
return mock.UpdateFunc(comment)
|
||||
}
|
||||
|
||||
// UpdateCalls gets all the calls that were made to Update.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.UpdateCalls())
|
||||
func (mock *InterfaceMock) UpdateCalls() []struct {
|
||||
Comment store.Comment
|
||||
} {
|
||||
var calls []struct {
|
||||
Comment store.Comment
|
||||
}
|
||||
mock.lockUpdate.RLock()
|
||||
calls = mock.calls.Update
|
||||
mock.lockUpdate.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// UserDetail calls UserDetailFunc.
|
||||
func (mock *InterfaceMock) UserDetail(req UserDetailRequest) ([]UserDetailEntry, error) {
|
||||
if mock.UserDetailFunc == nil {
|
||||
panic("InterfaceMock.UserDetailFunc: method is nil but Interface.UserDetail was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Req UserDetailRequest
|
||||
}{
|
||||
Req: req,
|
||||
}
|
||||
mock.lockUserDetail.Lock()
|
||||
mock.calls.UserDetail = append(mock.calls.UserDetail, callInfo)
|
||||
mock.lockUserDetail.Unlock()
|
||||
return mock.UserDetailFunc(req)
|
||||
}
|
||||
|
||||
// UserDetailCalls gets all the calls that were made to UserDetail.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInterface.UserDetailCalls())
|
||||
func (mock *InterfaceMock) UserDetailCalls() []struct {
|
||||
Req UserDetailRequest
|
||||
} {
|
||||
var calls []struct {
|
||||
Req UserDetailRequest
|
||||
}
|
||||
mock.lockUserDetail.RLock()
|
||||
calls = mock.calls.UserDetail
|
||||
mock.lockUserDetail.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
@@ -71,14 +71,44 @@ func (r *RPC) Flag(req FlagRequest) (status bool, err error) {
|
||||
return status, err
|
||||
}
|
||||
|
||||
func unmarshalString(data []byte) ([]interface{}, error) {
|
||||
var strings []string
|
||||
if err := json.Unmarshal(data, &strings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]interface{}, 0, len(strings))
|
||||
for _, w := range strings {
|
||||
list = append(list, w)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func unmarshalBlockedUser(data []byte) ([]interface{}, error) {
|
||||
var blockedUsers []store.BlockedUser
|
||||
if err := json.Unmarshal(data, &blockedUsers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]interface{}, 0, len(blockedUsers))
|
||||
for _, w := range blockedUsers {
|
||||
list = append(list, w)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// ListFlags get list of flagged keys, like blocked & verified user
|
||||
func (r *RPC) ListFlags(req FlagRequest) (list []interface{}, err error) {
|
||||
func (r *RPC) ListFlags(req FlagRequest) ([]interface{}, error) {
|
||||
resp, err := r.Call("store.list_flags", req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(*resp.Result, &list)
|
||||
return list, err
|
||||
// try []store.BlockedUser
|
||||
list, err := unmarshalBlockedUser(*resp.Result)
|
||||
if err == nil {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// try []strings
|
||||
return unmarshalString(*resp.Result)
|
||||
}
|
||||
|
||||
// UserDetail sets or gets single detail value, or gets all details for requested site.
|
||||
|
||||
@@ -134,7 +134,13 @@ func TestRemote_ListFlag(t *testing.T) {
|
||||
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
|
||||
res, err := c.ListFlags(FlagRequest{Locator: store.Locator{SiteID: "site_id"}, Flag: Blocked})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []interface{}{map[string]interface{}{"ID": "id1"}, map[string]interface{}{"ID": "id2"}}, res)
|
||||
require.Equal(t, 2, len(res))
|
||||
blockedUsers := make([]store.BlockedUser, 0, len(res))
|
||||
for _, v := range res {
|
||||
blockedUsers = append(blockedUsers, v.(store.BlockedUser))
|
||||
}
|
||||
assert.Equal(t, 2, len(blockedUsers))
|
||||
assert.Equal(t, []store.BlockedUser{{ID: "id1"}, {ID: "id2"}}, blockedUsers)
|
||||
}
|
||||
|
||||
func TestRemote_UserDetail(t *testing.T) {
|
||||
@@ -180,11 +186,17 @@ func TestRemote_Delete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemote_Close(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"store.close","id":1}`, `{}`)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "{\"method\":\"store.close\",\"id\":")
|
||||
t.Logf("req: %s", string(body))
|
||||
_, _ = fmt.Fprint(w, "{}")
|
||||
}))
|
||||
defer ts.Close()
|
||||
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
|
||||
err := c.Close()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, c.Close())
|
||||
assert.NoError(t, c.Close(), "second call should not result in panic or errors")
|
||||
}
|
||||
|
||||
func testServer(t *testing.T, req, resp string) *httptest.Server {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/Depado/bfchroma"
|
||||
"github.com/Depado/bfchroma/v2"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/alecthomas/chroma/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
bf "github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
@@ -42,17 +42,8 @@ func (f *CommentFormatter) Format(c Comment) Comment {
|
||||
|
||||
// FormatText converts text with markdown processor, applies external converters and shortens links
|
||||
func (f *CommentFormatter) FormatText(txt string) (res string) {
|
||||
mdExt := bf.NoIntraEmphasis | bf.Tables | bf.FencedCode |
|
||||
bf.Strikethrough | bf.SpaceHeadings | bf.HardLineBreak |
|
||||
bf.BackslashLineBreak | bf.Autolink
|
||||
|
||||
rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{
|
||||
Flags: bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes,
|
||||
})
|
||||
|
||||
extRend := bfchroma.NewRenderer(bfchroma.Extend(rend), bfchroma.ChromaOptions(html.WithClasses(true)))
|
||||
|
||||
res = string(bf.Run([]byte(txt), bf.WithExtensions(mdExt), bf.WithRenderer(extRend)))
|
||||
mdExt, rend := GetMdExtensionsAndRenderer()
|
||||
res = string(bf.Run([]byte(txt), bf.WithExtensions(mdExt), bf.WithRenderer(rend)))
|
||||
res = f.unEscape(res)
|
||||
|
||||
for _, conv := range f.converters {
|
||||
@@ -126,3 +117,18 @@ func (f *CommentFormatter) lazyImage(commentHTML string) (resHTML string) {
|
||||
}
|
||||
return resHTML
|
||||
}
|
||||
|
||||
// GetMdExtensionsAndRenderer returns blackfriday extensions and renderer used for rendering markdown
|
||||
// within store module.
|
||||
func GetMdExtensionsAndRenderer() (bf.Extensions, *bfchroma.Renderer) {
|
||||
mdExt := bf.NoIntraEmphasis | bf.Tables | bf.FencedCode |
|
||||
bf.Strikethrough | bf.SpaceHeadings | bf.HardLineBreak |
|
||||
bf.BackslashLineBreak | bf.Autolink
|
||||
|
||||
rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{
|
||||
Flags: bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes,
|
||||
})
|
||||
|
||||
extRend := bfchroma.NewRenderer(bfchroma.Extend(rend), bfchroma.ChromaOptions(html.WithClasses(true)))
|
||||
return mdExt, extRend
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ func TestFormatter_FormatText(t *testing.T) {
|
||||
},
|
||||
{"— not translated #354", "<p>— not translated #354</p>\n!converted", "mdash"},
|
||||
{"smth\n```go\nfunc main(aa string) int {return 0}\n```", `<p>smth</p>
|
||||
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
||||
<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
||||
</span></span></code></pre>!converted`, "code with language"},
|
||||
{"```\ntest_code\n```", `<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">test_code
|
||||
{"```\ntest_code\n```", `<pre class="chroma"><code><span class="line"><span class="cl">test_code
|
||||
</span></span></code></pre>!converted`, "code without language"},
|
||||
}
|
||||
f := NewCommentFormatter(mockConverter{})
|
||||
|
||||
@@ -2,7 +2,6 @@ package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
@@ -150,7 +149,7 @@ func checkBoltImgData(t *testing.T, db *bolt.DB, bucket, id string, callback fun
|
||||
}
|
||||
|
||||
func prepareBoltImageStorageTest(t *testing.T) (svc *Bolt, teardown func()) {
|
||||
loc, err := ioutil.TempDir("", "test_image_r42")
|
||||
loc, err := os.MkdirTemp("", "test_image_r42")
|
||||
require.NoError(t, err, "failed to make temp dir")
|
||||
|
||||
svc, err = NewBoltStorage(path.Join(loc, "picture.db"), bolt.Options{})
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
@@ -181,7 +180,7 @@ func TestFsStore_Cleanup(t *testing.T) {
|
||||
svc, teardown := prepareImageTest(t)
|
||||
defer teardown()
|
||||
|
||||
save := func(file string, user string) (filePath string) {
|
||||
save := func(file, user string) (filePath string) {
|
||||
id := path.Join(user, file)
|
||||
err := svc.Save(id, gopherPNGBytes())
|
||||
require.NoError(t, err)
|
||||
@@ -228,7 +227,7 @@ func TestFsStore_Cleanup(t *testing.T) {
|
||||
_, err = os.Stat(img2)
|
||||
assert.Error(t, err, "no file on staging anymore")
|
||||
_, err = os.Stat(img3)
|
||||
assert.NoError(t, err, "third image is still on staging because it's cleanup timer was reset")
|
||||
assert.NoError(t, err, "third image is still on staging because its cleanup timer was reset")
|
||||
|
||||
err = svc.ResetCleanupTimer("unknown_image.png")
|
||||
assert.Error(t, err)
|
||||
@@ -254,10 +253,10 @@ func TestFsStore_Info(t *testing.T) {
|
||||
}
|
||||
|
||||
func prepareImageTest(t *testing.T) (svc *FileSystem, teardown func()) {
|
||||
loc, err := ioutil.TempDir("", "test_image_r42")
|
||||
loc, err := os.MkdirTemp("", "test_image_r42")
|
||||
require.NoError(t, err, "failed to make temp dir")
|
||||
|
||||
staging, err := ioutil.TempDir("", "test_image_r42.staging")
|
||||
staging, err := os.MkdirTemp("", "test_image_r42.staging")
|
||||
require.NoError(t, err, "failed to make temp staging dir")
|
||||
|
||||
svc = &FileSystem{
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// Service object encloses Store and add common methods, this is the one consumer should use.
|
||||
package image
|
||||
|
||||
//go:generate sh -c "mockery -inpkg -name Store -print > /tmp/mock.tmp && mv /tmp/mock.tmp image_mock.go"
|
||||
// NOTE: matryer/moq should be installed globally and works with `go generate ./...`
|
||||
//go:generate moq --out image_mock.go . Store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -61,9 +62,6 @@ type StoreInfo struct {
|
||||
FirstStagingImageTS time.Time
|
||||
}
|
||||
|
||||
// To regenerate mock run from this directory:
|
||||
// sh -c "mockery -inpkg -name Store -print > /tmp/image-mock.tmp && mv /tmp/image-mock.tmp image_mock.go"
|
||||
|
||||
// Store defines interface for saving and loading pictures.
|
||||
// Declares two-stage save with Commit. Save stores to staging area and Commit moves to the final location.
|
||||
// Two-stage commit scheme is used for not storing images which are uploaded but later never used in the comments,
|
||||
@@ -90,8 +88,8 @@ func NewService(s Store, p ServiceParams) *Service {
|
||||
return &Service{ServiceParams: p, store: s}
|
||||
}
|
||||
|
||||
// SubmitAndCommit multiple ids immediately
|
||||
func (s *Service) SubmitAndCommit(idsFn func() []string) error {
|
||||
// Commit multiple ids immediately
|
||||
func (s *Service) Commit(idsFn func() []string) error {
|
||||
errs := new(multierror.Error)
|
||||
for _, id := range idsFn() {
|
||||
err := s.store.Commit(id)
|
||||
@@ -119,7 +117,7 @@ func (s *Service) Submit(idsFn func() []string) {
|
||||
for atomic.LoadInt32(&s.term) == 0 && time.Since(req.TS) <= s.EditDuration {
|
||||
time.Sleep(time.Millisecond * 10) // small sleep to relive busy wait but keep reactive for term (close)
|
||||
}
|
||||
err := s.SubmitAndCommit(req.idsFn)
|
||||
err := s.Commit(req.idsFn)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] image commit error %v", err)
|
||||
}
|
||||
@@ -143,39 +141,14 @@ func (s *Service) Submit(idsFn func() []string) {
|
||||
|
||||
// ExtractPictures gets list of images from the doc html and convert from urls to ids, i.e. user/pic.png
|
||||
func (s *Service) ExtractPictures(commentHTML string) (ids []string) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] can't parse commentHTML to parse images: %q, error: %v", commentHTML, err)
|
||||
return nil
|
||||
}
|
||||
doc.Find("img").Each(func(i int, sl *goquery.Selection) {
|
||||
if im, ok := sl.Attr("src"); ok {
|
||||
if strings.Contains(im, s.ImageAPI) {
|
||||
elems := strings.Split(im, "/")
|
||||
if len(elems) >= 2 {
|
||||
id := elems[len(elems)-2] + "/" + elems[len(elems)-1]
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if strings.Contains(im, s.ProxyAPI) {
|
||||
proxiedURL, err := url.Parse(im)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
imgURL, err := base64.URLEncoding.DecodeString(proxiedURL.Query().Get("src"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
imgID, err := CachedImgID(string(imgURL))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ids = append(ids, imgID)
|
||||
}
|
||||
}
|
||||
})
|
||||
return s.extractImageIDs(commentHTML, true)
|
||||
}
|
||||
|
||||
return ids
|
||||
// ExtractNonProxiedPictures gets list of non-proxied images from the doc html and convert from urls to ids, i.e. user/pic.png
|
||||
// This method is used in image check on post preview and load, as proxied images have lazy loading
|
||||
// and wouldn't be present on disk but still valid as they will be loaded the first time someone requests them.
|
||||
func (s *Service) ExtractNonProxiedPictures(commentHTML string) (ids []string) {
|
||||
return s.extractImageIDs(commentHTML, false)
|
||||
}
|
||||
|
||||
// Cleanup runs periodic cleanup with 1.5*ServiceParams.EditDuration. Blocking loop, should be called inside of goroutine by consumer
|
||||
@@ -264,6 +237,43 @@ func (s *Service) ImgContentType(img []byte) string {
|
||||
return contentType
|
||||
}
|
||||
|
||||
// returns list of image IDs from the comment html, including proxied images if includeProxied is true
|
||||
func (s *Service) extractImageIDs(commentHTML string, includeProxied bool) (ids []string) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] can't parse commentHTML to parse images: %q, error: %v", commentHTML, err)
|
||||
return nil
|
||||
}
|
||||
doc.Find("img").Each(func(i int, sl *goquery.Selection) {
|
||||
if im, ok := sl.Attr("src"); ok {
|
||||
if strings.Contains(im, s.ImageAPI) {
|
||||
elems := strings.Split(im, "/")
|
||||
if len(elems) >= 2 {
|
||||
id := elems[len(elems)-2] + "/" + elems[len(elems)-1]
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if includeProxied && strings.Contains(im, s.ProxyAPI) {
|
||||
proxiedURL, err := url.Parse(im)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
imgURL, err := base64.URLEncoding.DecodeString(proxiedURL.Query().Get("src"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
imgID, err := CachedImgID(string(imgURL))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ids = append(ids, imgID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// prepareImage calls readAndValidateImage and resize on provided image.
|
||||
func (s *Service) prepareImage(r io.Reader) ([]byte, error) {
|
||||
data, err := readAndValidateImage(r, s.MaxSize)
|
||||
@@ -367,7 +377,7 @@ func Sha1Str(s string) string {
|
||||
// ID would look like: "cached_images/<sha1-of-image-url-hostname>-<sha1-of-image-entire-url>"
|
||||
// <sha1-of-image-url-hostname> - would allow us to identify all images from particular site if ever needed
|
||||
// <sha1-of-image-entire-url> - would allow us to avoid storing duplicates of the same image
|
||||
// (as accurate as deduplication based on potentially mutable url can be)
|
||||
// (as accurate as deduplication based on potentially mutable url can be)
|
||||
func CachedImgID(imgURL string) (string, error) {
|
||||
parsedURL, err := url.Parse(imgURL)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,115 +1,301 @@
|
||||
// Code generated by mockery v1.1.2. DO NOT EDIT.
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
context "context"
|
||||
"sync"
|
||||
time "time"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockStore is an autogenerated mock type for the Store type
|
||||
type MockStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
// Ensure, that StoreMock does implement Store.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ Store = &StoreMock{}
|
||||
|
||||
// Cleanup provides a mock function with given fields: ctx, ttl
|
||||
func (_m *MockStore) Cleanup(ctx context.Context, ttl time.Duration) error {
|
||||
ret := _m.Called(ctx, ttl)
|
||||
// StoreMock is a mock implementation of Store.
|
||||
//
|
||||
// func TestSomethingThatUsesStore(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked Store
|
||||
// mockedStore := &StoreMock{
|
||||
// CleanupFunc: func(ctx context.Context, ttl time.Duration) error {
|
||||
// panic("mock out the Cleanup method")
|
||||
// },
|
||||
// CommitFunc: func(id string) error {
|
||||
// panic("mock out the Commit method")
|
||||
// },
|
||||
// InfoFunc: func() (StoreInfo, error) {
|
||||
// panic("mock out the Info method")
|
||||
// },
|
||||
// LoadFunc: func(id string) ([]byte, error) {
|
||||
// panic("mock out the Load method")
|
||||
// },
|
||||
// ResetCleanupTimerFunc: func(id string) error {
|
||||
// panic("mock out the ResetCleanupTimer method")
|
||||
// },
|
||||
// SaveFunc: func(id string, img []byte) error {
|
||||
// panic("mock out the Save method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedStore in code that requires Store
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type StoreMock struct {
|
||||
// CleanupFunc mocks the Cleanup method.
|
||||
CleanupFunc func(ctx context.Context, ttl time.Duration) error
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, time.Duration) error); ok {
|
||||
r0 = rf(ctx, ttl)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
// CommitFunc mocks the Commit method.
|
||||
CommitFunc func(id string) error
|
||||
|
||||
return r0
|
||||
}
|
||||
// InfoFunc mocks the Info method.
|
||||
InfoFunc func() (StoreInfo, error)
|
||||
|
||||
// Commit provides a mock function with given fields: id
|
||||
func (_m *MockStore) Commit(id string) error {
|
||||
ret := _m.Called(id)
|
||||
// LoadFunc mocks the Load method.
|
||||
LoadFunc func(id string) ([]byte, error)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
// ResetCleanupTimerFunc mocks the ResetCleanupTimer method.
|
||||
ResetCleanupTimerFunc func(id string) error
|
||||
|
||||
return r0
|
||||
}
|
||||
// SaveFunc mocks the Save method.
|
||||
SaveFunc func(id string, img []byte) error
|
||||
|
||||
// Info provides a mock function with given fields:
|
||||
func (_m *MockStore) Info() (StoreInfo, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 StoreInfo
|
||||
if rf, ok := ret.Get(0).(func() StoreInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(StoreInfo)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields: id
|
||||
func (_m *MockStore) Load(id string) ([]byte, error) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 []byte
|
||||
if rf, ok := ret.Get(0).(func(string) []byte); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// Cleanup holds details about calls to the Cleanup method.
|
||||
Cleanup []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// TTL is the ttl argument value.
|
||||
TTL time.Duration
|
||||
}
|
||||
// Commit holds details about calls to the Commit method.
|
||||
Commit []struct {
|
||||
// ID is the id argument value.
|
||||
ID string
|
||||
}
|
||||
// Info holds details about calls to the Info method.
|
||||
Info []struct {
|
||||
}
|
||||
// Load holds details about calls to the Load method.
|
||||
Load []struct {
|
||||
// ID is the id argument value.
|
||||
ID string
|
||||
}
|
||||
// ResetCleanupTimer holds details about calls to the ResetCleanupTimer method.
|
||||
ResetCleanupTimer []struct {
|
||||
// ID is the id argument value.
|
||||
ID string
|
||||
}
|
||||
// Save holds details about calls to the Save method.
|
||||
Save []struct {
|
||||
// ID is the id argument value.
|
||||
ID string
|
||||
// Img is the img argument value.
|
||||
Img []byte
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
lockCleanup sync.RWMutex
|
||||
lockCommit sync.RWMutex
|
||||
lockInfo sync.RWMutex
|
||||
lockLoad sync.RWMutex
|
||||
lockResetCleanupTimer sync.RWMutex
|
||||
lockSave sync.RWMutex
|
||||
}
|
||||
|
||||
// ResetCleanupTimer provides a mock function with given fields: id
|
||||
func (_m *MockStore) ResetCleanupTimer(id string) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
// Cleanup calls CleanupFunc.
|
||||
func (mock *StoreMock) Cleanup(ctx context.Context, ttl time.Duration) error {
|
||||
if mock.CleanupFunc == nil {
|
||||
panic("StoreMock.CleanupFunc: method is nil but Store.Cleanup was just called")
|
||||
}
|
||||
|
||||
return r0
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
TTL time.Duration
|
||||
}{
|
||||
Ctx: ctx,
|
||||
TTL: ttl,
|
||||
}
|
||||
mock.lockCleanup.Lock()
|
||||
mock.calls.Cleanup = append(mock.calls.Cleanup, callInfo)
|
||||
mock.lockCleanup.Unlock()
|
||||
return mock.CleanupFunc(ctx, ttl)
|
||||
}
|
||||
|
||||
// Save provides a mock function with given fields: id, img
|
||||
func (_m *MockStore) Save(id string, img []byte) error {
|
||||
ret := _m.Called(id, img)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, []byte) error); ok {
|
||||
r0 = rf(id, img)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
// CleanupCalls gets all the calls that were made to Cleanup.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.CleanupCalls())
|
||||
func (mock *StoreMock) CleanupCalls() []struct {
|
||||
Ctx context.Context
|
||||
TTL time.Duration
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
TTL time.Duration
|
||||
}
|
||||
mock.lockCleanup.RLock()
|
||||
calls = mock.calls.Cleanup
|
||||
mock.lockCleanup.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
return r0
|
||||
// Commit calls CommitFunc.
|
||||
func (mock *StoreMock) Commit(id string) error {
|
||||
if mock.CommitFunc == nil {
|
||||
panic("StoreMock.CommitFunc: method is nil but Store.Commit was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ID string
|
||||
}{
|
||||
ID: id,
|
||||
}
|
||||
mock.lockCommit.Lock()
|
||||
mock.calls.Commit = append(mock.calls.Commit, callInfo)
|
||||
mock.lockCommit.Unlock()
|
||||
return mock.CommitFunc(id)
|
||||
}
|
||||
|
||||
// CommitCalls gets all the calls that were made to Commit.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.CommitCalls())
|
||||
func (mock *StoreMock) CommitCalls() []struct {
|
||||
ID string
|
||||
} {
|
||||
var calls []struct {
|
||||
ID string
|
||||
}
|
||||
mock.lockCommit.RLock()
|
||||
calls = mock.calls.Commit
|
||||
mock.lockCommit.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Info calls InfoFunc.
|
||||
func (mock *StoreMock) Info() (StoreInfo, error) {
|
||||
if mock.InfoFunc == nil {
|
||||
panic("StoreMock.InfoFunc: method is nil but Store.Info was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockInfo.Lock()
|
||||
mock.calls.Info = append(mock.calls.Info, callInfo)
|
||||
mock.lockInfo.Unlock()
|
||||
return mock.InfoFunc()
|
||||
}
|
||||
|
||||
// InfoCalls gets all the calls that were made to Info.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.InfoCalls())
|
||||
func (mock *StoreMock) InfoCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
mock.lockInfo.RLock()
|
||||
calls = mock.calls.Info
|
||||
mock.lockInfo.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Load calls LoadFunc.
|
||||
func (mock *StoreMock) Load(id string) ([]byte, error) {
|
||||
if mock.LoadFunc == nil {
|
||||
panic("StoreMock.LoadFunc: method is nil but Store.Load was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ID string
|
||||
}{
|
||||
ID: id,
|
||||
}
|
||||
mock.lockLoad.Lock()
|
||||
mock.calls.Load = append(mock.calls.Load, callInfo)
|
||||
mock.lockLoad.Unlock()
|
||||
return mock.LoadFunc(id)
|
||||
}
|
||||
|
||||
// LoadCalls gets all the calls that were made to Load.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.LoadCalls())
|
||||
func (mock *StoreMock) LoadCalls() []struct {
|
||||
ID string
|
||||
} {
|
||||
var calls []struct {
|
||||
ID string
|
||||
}
|
||||
mock.lockLoad.RLock()
|
||||
calls = mock.calls.Load
|
||||
mock.lockLoad.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ResetCleanupTimer calls ResetCleanupTimerFunc.
|
||||
func (mock *StoreMock) ResetCleanupTimer(id string) error {
|
||||
if mock.ResetCleanupTimerFunc == nil {
|
||||
panic("StoreMock.ResetCleanupTimerFunc: method is nil but Store.ResetCleanupTimer was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ID string
|
||||
}{
|
||||
ID: id,
|
||||
}
|
||||
mock.lockResetCleanupTimer.Lock()
|
||||
mock.calls.ResetCleanupTimer = append(mock.calls.ResetCleanupTimer, callInfo)
|
||||
mock.lockResetCleanupTimer.Unlock()
|
||||
return mock.ResetCleanupTimerFunc(id)
|
||||
}
|
||||
|
||||
// ResetCleanupTimerCalls gets all the calls that were made to ResetCleanupTimer.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.ResetCleanupTimerCalls())
|
||||
func (mock *StoreMock) ResetCleanupTimerCalls() []struct {
|
||||
ID string
|
||||
} {
|
||||
var calls []struct {
|
||||
ID string
|
||||
}
|
||||
mock.lockResetCleanupTimer.RLock()
|
||||
calls = mock.calls.ResetCleanupTimer
|
||||
mock.lockResetCleanupTimer.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Save calls SaveFunc.
|
||||
func (mock *StoreMock) Save(id string, img []byte) error {
|
||||
if mock.SaveFunc == nil {
|
||||
panic("StoreMock.SaveFunc: method is nil but Store.Save was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ID string
|
||||
Img []byte
|
||||
}{
|
||||
ID: id,
|
||||
Img: img,
|
||||
}
|
||||
mock.lockSave.Lock()
|
||||
mock.calls.Save = append(mock.calls.Save, callInfo)
|
||||
mock.lockSave.Unlock()
|
||||
return mock.SaveFunc(id, img)
|
||||
}
|
||||
|
||||
// SaveCalls gets all the calls that were made to Save.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedStore.SaveCalls())
|
||||
func (mock *StoreMock) SaveCalls() []struct {
|
||||
ID string
|
||||
Img []byte
|
||||
} {
|
||||
var calls []struct {
|
||||
ID string
|
||||
Img []byte
|
||||
}
|
||||
mock.lockSave.RLock()
|
||||
calls = mock.calls.Save
|
||||
mock.lockSave.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
@@ -14,22 +14,30 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_SaveAndLoad(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store := StoreMock{
|
||||
SaveFunc: func(id string, img []byte) error {
|
||||
return nil
|
||||
},
|
||||
LoadFunc: func(id string) ([]byte, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
svc := NewService(&store, ServiceParams{MaxSize: 1500, MaxWidth: 32, MaxHeight: 32})
|
||||
|
||||
store.On("Save", "test_id", mock.Anything).Return(nil)
|
||||
err := svc.SaveWithID("test_id", gopherPNG())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(store.SaveCalls()))
|
||||
assert.Equal(t, "test_id", store.SaveCalls()[0].ID)
|
||||
|
||||
store.On("Load", "test_id", mock.Anything).Return(nil, nil)
|
||||
img, err := svc.Load("test_id")
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, img)
|
||||
assert.Equal(t, 1, len(store.LoadCalls()))
|
||||
assert.Equal(t, "test_id", store.LoadCalls()[0].ID)
|
||||
}
|
||||
|
||||
func TestService_Resize(t *testing.T) {
|
||||
@@ -97,6 +105,7 @@ func TestService_ExtractPictures(t *testing.T) {
|
||||
ids = svc.ExtractPictures(html)
|
||||
require.Equal(t, 1, len(ids), "one image in")
|
||||
assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", ids[0])
|
||||
require.Empty(t, svc.ExtractNonProxiedPictures(html), "no non-proxied images expected to be found")
|
||||
|
||||
// bad url
|
||||
html = `<img src=" https://remark42.radio-t.com/api/v1/img">`
|
||||
@@ -116,72 +125,91 @@ func TestService_ExtractPictures(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_Cleanup(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("Cleanup", mock.Anything, mock.Anything).Times(10).Return(nil)
|
||||
store := StoreMock{
|
||||
CleanupFunc: func(ctx context.Context, ttl time.Duration) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
||||
// cancel context after 2.1 cleanup TTLs
|
||||
ctx, cancel := context.WithTimeout(context.Background(), svc.EditDuration/100*15*21)
|
||||
defer cancel()
|
||||
svc.Cleanup(ctx)
|
||||
store.AssertNumberOfCalls(t, "Cleanup", 2)
|
||||
assert.Equal(t, 2, len(store.CleanupCalls()))
|
||||
}
|
||||
|
||||
func TestService_Submit(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("Commit", mock.Anything, mock.Anything).Times(7).Return(nil)
|
||||
store.On("ResetCleanupTimer", mock.Anything, mock.Anything).Times(7).Return(nil)
|
||||
store := StoreMock{
|
||||
CommitFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := NewService(&store, ServiceParams{ImageAPI: "/blah/", EditDuration: time.Millisecond * 100})
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 3)
|
||||
err := svc.SubmitAndCommit(func() []string { return []string{"id4", "id5"} })
|
||||
assert.Equal(t, 3, len(store.ResetCleanupTimerCalls()))
|
||||
err := svc.Commit(func() []string { return []string{"id4", "id5"} })
|
||||
assert.NoError(t, err)
|
||||
svc.Submit(func() []string { return []string{"id6", "id7"} })
|
||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 5)
|
||||
assert.Equal(t, 5, len(store.ResetCleanupTimerCalls()))
|
||||
svc.Submit(nil)
|
||||
store.AssertNumberOfCalls(t, "Commit", 2)
|
||||
assert.Equal(t, 2, len(store.CommitCalls()))
|
||||
time.Sleep(time.Millisecond * 175)
|
||||
store.AssertNumberOfCalls(t, "Commit", 7)
|
||||
assert.Equal(t, 7, len(store.CommitCalls()))
|
||||
svc.Close(context.TODO())
|
||||
}
|
||||
|
||||
func TestService_Close(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("Commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
store.On("ResetCleanupTimer", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
store := StoreMock{
|
||||
CommitFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := Service{store: &store, ServiceParams: ServiceParams{ImageAPI: "/blah/", EditDuration: time.Hour * 24}}
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 5)
|
||||
assert.Equal(t, 5, len(store.ResetCleanupTimerCalls()))
|
||||
svc.Close(context.TODO())
|
||||
store.AssertNumberOfCalls(t, "Commit", 5)
|
||||
assert.Equal(t, 5, len(store.CommitCalls()))
|
||||
}
|
||||
|
||||
func TestService_SubmitDelay(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("Commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
store.On("ResetCleanupTimer", mock.Anything, mock.Anything).Times(5).Return(nil)
|
||||
store := StoreMock{
|
||||
CommitFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||
time.Sleep(150 * time.Millisecond) // let first batch to pass TTL
|
||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||
svc.Submit(nil)
|
||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 5)
|
||||
store.AssertNumberOfCalls(t, "Commit", 3)
|
||||
assert.Equal(t, 5, len(store.ResetCleanupTimerCalls()))
|
||||
assert.Equal(t, 3, len(store.CommitCalls()))
|
||||
svc.Close(context.TODO())
|
||||
store.AssertNumberOfCalls(t, "Commit", 5)
|
||||
assert.Equal(t, 5, len(store.CommitCalls()))
|
||||
}
|
||||
|
||||
func TestService_Info(t *testing.T) {
|
||||
store := MockStore{}
|
||||
store.On("Info", mock.Anything, mock.Anything).Once().Return(StoreInfo{}, nil)
|
||||
store := StoreMock{InfoFunc: func() (StoreInfo, error) {
|
||||
return StoreInfo{}, nil
|
||||
}}
|
||||
|
||||
svc := Service{store: &store, ServiceParams: ServiceParams{}}
|
||||
info, err := svc.Info()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.FirstStagingImageTS.IsZero())
|
||||
store.AssertNumberOfCalls(t, "Info", 1)
|
||||
assert.Equal(t, 1, len(store.InfoCalls()))
|
||||
}
|
||||
|
||||
func TestService_resize(t *testing.T) {
|
||||
@@ -259,3 +287,11 @@ func TestCachedImgID(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "cached_images/"+Sha1Str("example.org")+"-"+Sha1Str(imgURL), img)
|
||||
}
|
||||
|
||||
func TestService_DoubleClose(*testing.T) {
|
||||
store := StoreMock{}
|
||||
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
||||
svc.Close(context.TODO())
|
||||
// second call should not result in panic
|
||||
svc.Close(context.TODO())
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -14,6 +14,7 @@ import (
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
bf "github.com/russross/blackfriday/v2"
|
||||
|
||||
"github.com/umputun/remark42/backend/app/store"
|
||||
"github.com/umputun/remark42/backend/app/store/admin"
|
||||
@@ -278,7 +279,7 @@ func (s *DataStore) submitImages(comment store.Comment) {
|
||||
|
||||
var err error
|
||||
if comment.Imported {
|
||||
err = s.ImageService.SubmitAndCommit(idsFn)
|
||||
err = s.ImageService.Commit(idsFn)
|
||||
} else {
|
||||
s.ImageService.Submit(idsFn)
|
||||
}
|
||||
@@ -496,6 +497,12 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi
|
||||
if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvDelete); e != nil {
|
||||
log.Printf("[WARN] failed to send delete event, %s", e)
|
||||
}
|
||||
// clean up the comment and its parent from cache, so that
|
||||
// after cleaning up the child, parent won't be stuck non-deletable till cache expires
|
||||
if s.repliesCache.LoadingCache != nil {
|
||||
s.repliesCache.Delete(commentID)
|
||||
s.repliesCache.Delete(comment.ParentID)
|
||||
}
|
||||
comment.Deleted = true
|
||||
delReq := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: store.SoftDelete}
|
||||
return comment, s.Engine.Delete(delReq)
|
||||
@@ -621,7 +628,9 @@ func (s *DataStore) Counts(siteID string, postIDs []string) ([]store.PostInfo, e
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ValidateComment checks if comment size below max and user fields set
|
||||
// ValidateComment checks if comment size below max and user fields set.
|
||||
// It also validates the absence of relative links as they are almost never the intention of the commenter,
|
||||
// usually added by mistakes and only create confusion.
|
||||
func (s *DataStore) ValidateComment(c *store.Comment) error {
|
||||
maxSize := s.MaxCommentSize
|
||||
if s.MaxCommentSize <= 0 {
|
||||
@@ -636,7 +645,21 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
|
||||
if c.User.ID == "" || c.User.Name == "" {
|
||||
return fmt.Errorf("empty user info")
|
||||
}
|
||||
return nil
|
||||
|
||||
mdExt, rend := store.GetMdExtensionsAndRenderer()
|
||||
parser := bf.New(bf.WithRenderer(rend), bf.WithExtensions(bf.CommonExtensions), bf.WithExtensions(mdExt))
|
||||
var wrongLinkError error
|
||||
parser.Parse([]byte(c.Orig)).Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
|
||||
if len(node.LinkData.Destination) != 0 &&
|
||||
!(strings.HasPrefix(string(node.LinkData.Destination), "http://") ||
|
||||
strings.HasPrefix(string(node.LinkData.Destination), "https://") ||
|
||||
strings.HasPrefix(string(node.LinkData.Destination), "mailto:")) {
|
||||
wrongLinkError = fmt.Errorf("links should start with mailto:, http:// or https://")
|
||||
return bf.Terminate
|
||||
}
|
||||
return bf.GoToNext
|
||||
})
|
||||
return wrongLinkError
|
||||
}
|
||||
|
||||
// IsAdmin checks if usesID in the list of admins
|
||||
@@ -739,6 +762,17 @@ func (s *DataStore) Delete(locator store.Locator, commentID string, mode store.D
|
||||
if e := s.AdminStore.OnEvent(locator.SiteID, admin.EvDelete); e != nil {
|
||||
log.Printf("[WARN] failed to send delete event, %s", e)
|
||||
}
|
||||
// get comment to learn its parent ID
|
||||
comment, err := s.Engine.Get(engine.GetRequest{Locator: locator, CommentID: commentID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// clean up the comment and its parent from cache, so that
|
||||
// after cleaning up the child, parent won't be stuck non-deletable till cache expires
|
||||
if s.repliesCache.LoadingCache != nil {
|
||||
s.repliesCache.Delete(commentID)
|
||||
s.repliesCache.Delete(comment.ParentID)
|
||||
}
|
||||
req := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: mode}
|
||||
return s.Engine.Delete(req)
|
||||
}
|
||||
@@ -981,9 +1015,9 @@ func (s *DataStore) prepVotes(c store.Comment, user store.User) store.Comment {
|
||||
}
|
||||
|
||||
// get secret for given siteID
|
||||
// Note: secret shared across sites, but some sites can be disabled.
|
||||
// Note: siteID ignored for the default admin.Static store
|
||||
func (s *DataStore) getSecret(siteID string) (secret string, err error) {
|
||||
if secret, err = s.AdminStore.Key("any"); err != nil {
|
||||
if secret, err = s.AdminStore.Key(siteID); err != nil {
|
||||
return "", fmt.Errorf("can't get secret for site %s: %w", siteID, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -414,7 +413,6 @@ func TestService_VoteAggressive(t *testing.T) {
|
||||
assert.Equal(t, 0, len(res[0].VotedIPs), "vote ips hidden")
|
||||
|
||||
// random +1/-1 result should be [0..2]
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -781,22 +779,25 @@ func TestService_ValidateComment(t *testing.T) {
|
||||
|
||||
tbl := []struct {
|
||||
inp store.Comment
|
||||
err error
|
||||
err string
|
||||
}{
|
||||
{inp: store.Comment{}, err: fmt.Errorf("empty comment text")},
|
||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid", Name: "name"}}, err: nil},
|
||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid"}}, err: fmt.Errorf("empty user info")},
|
||||
{inp: store.Comment{Orig: longText, User: store.User{ID: "myid", Name: "name"}}, err: fmt.Errorf("comment text exceeded max allowed size 2000 (4000)")},
|
||||
{inp: store.Comment{}, err: "empty comment text"},
|
||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid", Name: "name"}}, err: ""},
|
||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid"}}, err: "empty user info"},
|
||||
{inp: store.Comment{Orig: longText, User: store.User{ID: "myid", Name: "name"}}, err: "comment text exceeded max allowed size 2000 (4000)"},
|
||||
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||
{inp: store.Comment{Orig: "multiple links, one is bad: [test](http://test) [test2](bad_url) [test3](https://test3)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||
}
|
||||
|
||||
for n, tt := range tbl {
|
||||
err := b.ValidateComment(&tt.inp)
|
||||
if tt.err == nil {
|
||||
if tt.err == "" {
|
||||
assert.NoError(t, err, "check #%d", n)
|
||||
continue
|
||||
}
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, tt.err, err.Error(), "check #%d", n)
|
||||
assert.EqualError(t, err, tt.err, "check #%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,7 +973,7 @@ func TestService_HasReplies(t *testing.T) {
|
||||
// two comments for https://radio-t.com, no reply
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
|
||||
b := DataStore{Engine: eng,
|
||||
AdminStore: admin.NewStaticStore("secret 123", []string{"radio-t"}, []string{"user2"}, "user@email.com")}
|
||||
defer b.Close()
|
||||
|
||||
@@ -983,11 +984,10 @@ func TestService_HasReplies(t *testing.T) {
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
|
||||
assert.False(t, b.HasReplies(comment))
|
||||
|
||||
reply := store.Comment{
|
||||
ID: "123456",
|
||||
ID: "c-1",
|
||||
ParentID: "id-1",
|
||||
Text: "some text",
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
@@ -996,7 +996,45 @@ func TestService_HasReplies(t *testing.T) {
|
||||
}
|
||||
_, err := b.Create(reply)
|
||||
assert.NoError(t, err)
|
||||
_, found := b.repliesCache.Peek(comment.ID)
|
||||
assert.False(t, found, "not yet checked")
|
||||
assert.True(t, b.HasReplies(comment))
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.True(t, found, "checked and has replies")
|
||||
|
||||
// deletion of the parent comment shouldn't work as the comment has replies
|
||||
_, err = b.EditComment(reply.Locator, comment.ID, EditRequest{Orig: comment.ID, Delete: true, Summary: "user deletes the comment"})
|
||||
assert.EqualError(t, err, "parent comment with reply can't be edited, "+comment.ID)
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.True(t, found, "checked and has replies")
|
||||
|
||||
// should not report replies after deletion of the child
|
||||
err = b.Delete(reply.Locator, reply.ID, store.HardDelete)
|
||||
assert.NoError(t, err)
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.False(t, found, "cleaned up from cache by Delete call")
|
||||
assert.False(t, b.HasReplies(comment))
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.False(t, found, "checked and has no replies")
|
||||
|
||||
// recreate reply with the new ID
|
||||
reply.ID = "c-2"
|
||||
_, err = b.Create(reply)
|
||||
assert.NoError(t, err)
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.False(t, found, "not yet checked")
|
||||
assert.True(t, b.HasReplies(comment))
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.True(t, found, "checked and has replies again")
|
||||
|
||||
// should not report replies after deletion of the child using Edit mechanism
|
||||
_, err = b.EditComment(reply.Locator, reply.ID, EditRequest{Orig: reply.ID, Delete: true, Summary: "user deletes the comment"})
|
||||
assert.NoError(t, err)
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.False(t, found, "cleaned up from cache by EditComment call")
|
||||
assert.False(t, b.HasReplies(comment))
|
||||
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||
assert.False(t, found, "checked and has no replies")
|
||||
}
|
||||
|
||||
func TestService_UserReplies(t *testing.T) {
|
||||
@@ -1359,11 +1397,10 @@ func TestService_DeleteAll(t *testing.T) {
|
||||
func TestService_submitImages(t *testing.T) {
|
||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||
|
||||
mockStore := image.MockStore{}
|
||||
mockStore.On("Commit", "dev/pic1.png").Once().Return(nil)
|
||||
mockStore.On("Commit", "dev/pic2.png").Once().Return(nil)
|
||||
mockStore.On("ResetCleanupTimer", "dev/pic1.png").Once().Return(nil)
|
||||
mockStore.On("ResetCleanupTimer", "dev/pic2.png").Once().Return(nil)
|
||||
mockStore := image.StoreMock{
|
||||
CommitFunc: func(id string) error { return nil },
|
||||
ResetCleanupTimerFunc: func(id string) error { return nil },
|
||||
}
|
||||
imgSvc := image.NewService(&mockStore,
|
||||
image.ServiceParams{
|
||||
EditDuration: 50 * time.Millisecond,
|
||||
@@ -1389,13 +1426,25 @@ func TestService_submitImages(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
b.submitImages(c)
|
||||
mockStore.AssertNumberOfCalls(t, "ResetCleanupTimer", 2)
|
||||
assert.Equal(t, 2, len(mockStore.ResetCleanupTimerCalls()))
|
||||
assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID)
|
||||
assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID)
|
||||
time.Sleep(b.EditDuration + 100*time.Millisecond)
|
||||
mockStore.AssertNumberOfCalls(t, "Commit", 2)
|
||||
assert.Equal(t, 2, len(mockStore.CommitCalls()))
|
||||
assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID)
|
||||
assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID)
|
||||
}
|
||||
|
||||
func TestService_ResubmitStagingImages(t *testing.T) {
|
||||
mockStore := image.MockStore{}
|
||||
mockStore := image.StoreMock{
|
||||
InfoFunc: func() (image.StoreInfo, error) {
|
||||
return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil
|
||||
},
|
||||
CommitFunc: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
ResetCleanupTimerFunc: func(id string) error { return nil },
|
||||
}
|
||||
imgSvc := image.NewService(&mockStore,
|
||||
image.ServiceParams{
|
||||
EditDuration: 10 * time.Millisecond,
|
||||
@@ -1408,10 +1457,6 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
|
||||
|
||||
mockStore.On("ResetCleanupTimer", "dev_user/bqf122eq9r8ad657n3ng").Once().Return(nil)
|
||||
mockStore.On("ResetCleanupTimer", "dev_user/bqf321eq9r8ad657n3ng").Once().Return(nil)
|
||||
mockStore.On("ResetCleanupTimer", "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e").Once().Return(nil)
|
||||
|
||||
// create comment with three images without preparing it properly
|
||||
comment := store.Comment{
|
||||
ID: "id-0",
|
||||
@@ -1427,21 +1472,19 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// resubmit single comment with three images, of which two are in staging storage
|
||||
mockStore.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil)
|
||||
err = b.ResubmitStagingImages([]string{"radio-t"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// wait for Submit goroutine to commit image
|
||||
mockStore.On("Commit", "dev_user/bqf122eq9r8ad657n3ng").Once().Return(nil)
|
||||
mockStore.On("Commit", "dev_user/bqf321eq9r8ad657n3ng").Once().Return(nil)
|
||||
mockStore.On("Commit", "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e").Once().Return(nil)
|
||||
time.Sleep(b.EditDuration + time.Millisecond*100)
|
||||
|
||||
mockStore.AssertNumberOfCalls(t, "Info", 1)
|
||||
mockStore.AssertNumberOfCalls(t, "Commit", 3)
|
||||
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||
assert.Equal(t, 3, len(mockStore.CommitCalls()))
|
||||
|
||||
// empty answer
|
||||
mockStoreEmpty := image.MockStore{}
|
||||
mockStoreEmpty := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
|
||||
return image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil
|
||||
}}
|
||||
imgSvcEmpty := image.NewService(&mockStoreEmpty,
|
||||
image.ServiceParams{
|
||||
EditDuration: 10 * time.Millisecond,
|
||||
@@ -1450,15 +1493,16 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
||||
defer imgSvcEmpty.Close(context.TODO())
|
||||
bEmpty := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcEmpty}
|
||||
|
||||
// resubmit receive empty timestamp and should do nothing
|
||||
mockStoreEmpty.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil)
|
||||
// resubmit receive empty timestamp and should do nothing )
|
||||
err = bEmpty.ResubmitStagingImages([]string{"radio-t", "non_existent"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockStoreEmpty.AssertNumberOfCalls(t, "Info", 1)
|
||||
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||
|
||||
// error from image storage
|
||||
mockStoreError := image.MockStore{}
|
||||
// error from image storage
|
||||
mockStoreError := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
|
||||
return image.StoreInfo{}, fmt.Errorf("mock_err")
|
||||
}}
|
||||
imgSvcError := image.NewService(&mockStoreError,
|
||||
image.ServiceParams{
|
||||
EditDuration: 10 * time.Millisecond,
|
||||
@@ -1468,15 +1512,20 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
||||
bError := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcError}
|
||||
|
||||
// resubmit will receive error from image storage and should return it
|
||||
mockStoreError.On("Info").Once().Return(image.StoreInfo{}, fmt.Errorf("mock_err"))
|
||||
err = bError.ResubmitStagingImages([]string{"radio-t"})
|
||||
assert.EqualError(t, err, "mock_err")
|
||||
|
||||
mockStoreError.AssertNumberOfCalls(t, "Info", 1)
|
||||
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||
assert.Equal(t, 3, len(mockStore.ResetCleanupTimerCalls()))
|
||||
assert.Equal(t, "dev_user/bqf122eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[0].ID)
|
||||
assert.Equal(t, "dev_user/bqf321eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[1].ID)
|
||||
assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", mockStore.ResetCleanupTimerCalls()[2].ID)
|
||||
}
|
||||
|
||||
func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
|
||||
mockStore := image.MockStore{}
|
||||
mockStore := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
|
||||
return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil
|
||||
}}
|
||||
imgSvc := image.NewService(&mockStore,
|
||||
image.ServiceParams{
|
||||
EditDuration: 10 * time.Millisecond,
|
||||
@@ -1484,26 +1533,37 @@ func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
|
||||
})
|
||||
defer imgSvc.Close(context.TODO())
|
||||
|
||||
engineMock := engine.MockInterface{}
|
||||
first := true
|
||||
engineMock := engine.InterfaceMock{
|
||||
FindFunc: func(req engine.FindRequest) ([]store.Comment, error) {
|
||||
if first {
|
||||
first = false
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("mockError")
|
||||
},
|
||||
}
|
||||
site1Req := engine.FindRequest{Locator: store.Locator{SiteID: "site1", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
|
||||
site2Req := engine.FindRequest{Locator: store.Locator{SiteID: "site2", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
|
||||
engineMock.On("Find", site1Req).Return(nil, nil)
|
||||
engineMock.On("Find", site2Req).Return(nil, fmt.Errorf("mockError"))
|
||||
b := DataStore{Engine: &engineMock, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
|
||||
|
||||
// One call without error and one with error
|
||||
mockStore.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil)
|
||||
err := b.ResubmitStagingImages([]string{"site1", "site2"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "problem finding comments for site site2: mockError")
|
||||
|
||||
mockStore.AssertNumberOfCalls(t, "Info", 1)
|
||||
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||
assert.Equal(t, 2, len(engineMock.FindCalls()))
|
||||
assert.Equal(t, site1Req, engineMock.FindCalls()[0].Req)
|
||||
assert.Equal(t, site2Req, engineMock.FindCalls()[1].Req)
|
||||
}
|
||||
|
||||
func TestService_alterComment(t *testing.T) {
|
||||
engineMock := engine.MockInterface{}
|
||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}).Return(false, nil)
|
||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}).Return(false, nil)
|
||||
engineMock := engine.InterfaceMock{
|
||||
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
svc := DataStore{Engine: &engineMock}
|
||||
|
||||
r := svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"},
|
||||
@@ -1514,24 +1574,48 @@ func TestService_alterComment(t *testing.T) {
|
||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}},
|
||||
store.User{Name: "dev", ID: "devid", Admin: true})
|
||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}}, r, "ip not cleaned")
|
||||
assert.Equal(t, 4, len(engineMock.FlagCalls()))
|
||||
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
|
||||
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[1].Req)
|
||||
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[2].Req)
|
||||
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[3].Req)
|
||||
|
||||
engineMock = engine.MockInterface{}
|
||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}).Return(false, nil)
|
||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}).Return(true, nil)
|
||||
first := true
|
||||
engineMock = engine.InterfaceMock{
|
||||
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||
if first {
|
||||
first = false
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
svc = DataStore{Engine: &engineMock}
|
||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true}},
|
||||
store.User{Name: "dev", ID: "devid", Admin: false})
|
||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", ID: "devid", Verified: true}}, r, "verified set")
|
||||
assert.Equal(t, 2, len(engineMock.FlagCalls()))
|
||||
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
|
||||
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[1].Req)
|
||||
|
||||
engineMock = engine.MockInterface{}
|
||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}).Return(true, nil)
|
||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}).Return(false, nil)
|
||||
first = true
|
||||
engineMock = engine.InterfaceMock{
|
||||
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||
if first {
|
||||
first = false
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
svc = DataStore{Engine: &engineMock}
|
||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true},
|
||||
Locator: store.Locator{URL: "javascript:alert('XSS1')"}},
|
||||
store.User{Name: "dev", ID: "devid", Admin: false})
|
||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", Verified: true, Blocked: true, ID: "devid"},
|
||||
Deleted: false}, r, "blocked")
|
||||
assert.Equal(t, 1, len(engineMock.FlagCalls()))
|
||||
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
|
||||
}
|
||||
|
||||
func Benchmark_ServiceCreate(b *testing.B) {
|
||||
@@ -1556,9 +1640,33 @@ func Benchmark_ServiceCreate(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_DoubleClose_Bolt(t *testing.T) {
|
||||
dbFile := fmt.Sprintf("%s/test-remark42-%d.db", os.TempDir(), rand.Intn(9999999999))
|
||||
defer func() { _ = os.Remove(dbFile) }()
|
||||
|
||||
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: dbFile, SiteID: "radio-t"})
|
||||
svc := DataStore{Engine: boltStore, EditDuration: 50 * time.Millisecond, AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, boltStore.Close())
|
||||
assert.NoError(t, boltStore.Close(), "second call should not result in panic or errors")
|
||||
assert.NoError(t, svc.Close())
|
||||
assert.NoError(t, svc.Close(), "second call should not result in panic or errors")
|
||||
}
|
||||
|
||||
func TestService_DoubleClose_Static(t *testing.T) {
|
||||
ks := admin.NewStaticKeyStore("secret 123")
|
||||
eng, teardown := prepStoreEngine(t)
|
||||
defer teardown()
|
||||
b := DataStore{Engine: eng, AdminStore: ks,
|
||||
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second})}
|
||||
b.Close()
|
||||
// second call should not result in panic or errors
|
||||
b.Close()
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
func prepStoreEngine(t *testing.T) (e engine.Interface, teardown func()) {
|
||||
testDBLoc, err := ioutil.TempDir("", "test_image_r42")
|
||||
testDBLoc, err := os.MkdirTemp("", "test_image_r42")
|
||||
require.NoError(t, err)
|
||||
testDB := path.Join(testDBLoc, "test.db")
|
||||
_ = os.Remove(testDB)
|
||||
|
||||
@@ -123,3 +123,10 @@ func TestTitle_GetFailed(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached")
|
||||
}
|
||||
|
||||
func TestTitle_DoubleClosed(*testing.T) {
|
||||
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
|
||||
ex.Close()
|
||||
// second call should not result in panic
|
||||
ex.Close()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestMakeTree(t *testing.T) {
|
||||
loc := store.Locator{URL: "url", SiteID: "site"}
|
||||
ts := func(min int, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
||||
ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
||||
|
||||
// unsorted by purpose
|
||||
comments := []store.Comment{
|
||||
@@ -54,7 +54,7 @@ func TestMakeTree(t *testing.T) {
|
||||
|
||||
func TestMakeEmptySubtree(t *testing.T) {
|
||||
loc := store.Locator{URL: "url", SiteID: "site"}
|
||||
ts := func(min int, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
||||
ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
||||
|
||||
// unsorted by purpose
|
||||
comments := []store.Comment{
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/rakyll/statik/fs"
|
||||
)
|
||||
|
||||
// FS stores link to statikFS if it exists
|
||||
type FS struct {
|
||||
statik http.FileSystem
|
||||
}
|
||||
//go:embed static
|
||||
var templateFS embed.FS
|
||||
|
||||
// FileReader describes methods of filesystem
|
||||
type FileReader interface {
|
||||
ReadFile(path string) ([]byte, error)
|
||||
}
|
||||
|
||||
// NewFS returns new FS instance, which will read from statik if it's available and from fs otherwise
|
||||
func NewFS() *FS {
|
||||
f := &FS{}
|
||||
if statikFS, err := fs.NewWithNamespace("templates"); err == nil {
|
||||
log.Printf("[INFO] templates will be read from statik")
|
||||
f.statik = statikFS
|
||||
// Read reads either template from disk if it exists, or from embedded template
|
||||
func Read(path string) ([]byte, error) {
|
||||
if _, err := os.Stat(filepath.Clean(path)); err == nil {
|
||||
return os.ReadFile(filepath.Clean(path))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// ReadFile depends on statik achieve exists
|
||||
func (f *FS) ReadFile(path string) ([]byte, error) {
|
||||
if f.statik != nil {
|
||||
return fs.ReadFile(f.statik, filepath.Join("/", path)) //nolint:gocritic // root folder is a requirement for statik
|
||||
// remove "static/" prefix from path
|
||||
var contentFS, err = fs.Sub(templateFS, "static")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(filepath.Clean(path))
|
||||
return fs.ReadFile(contentFS, filepath.Clean(path))
|
||||
}
|
||||
|
||||
@@ -6,19 +6,12 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewFS(t *testing.T) {
|
||||
fs := NewFS()
|
||||
assert.NotNil(t, &fs)
|
||||
}
|
||||
|
||||
func TestFS_ReadFile(t *testing.T) {
|
||||
fs := NewFS()
|
||||
|
||||
file, err := fs.ReadFile("testdata/template.html.tmpl")
|
||||
func Test_Read(t *testing.T) {
|
||||
file, err := Read("testdata/template.html.tmpl")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("template\n"), file)
|
||||
|
||||
file, err = fs.ReadFile("testdata/bad_path.html.tmpl")
|
||||
file, err = Read("testdata/bad_path.html.tmpl")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, file)
|
||||
}
|
||||
|
||||
+49
-50
@@ -1,79 +1,78 @@
|
||||
module github.com/umputun/remark42/backend
|
||||
|
||||
go 1.17
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/Depado/bfchroma v1.3.0
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/didip/tollbooth/v6 v6.1.2
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/Depado/bfchroma/v2 v2.0.0
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/alecthomas/chroma/v2 v2.8.0
|
||||
github.com/didip/tollbooth/v7 v7.0.1
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-pkgz/auth v1.19.0
|
||||
github.com/go-pkgz/jrpc v0.2.0
|
||||
github.com/go-pkgz/lcw v0.8.1
|
||||
github.com/go-pkgz/lgr v0.10.4
|
||||
github.com/go-pkgz/notify v0.1.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-pkgz/auth v1.21.1-0.20230820223834-cea049c06aa5
|
||||
github.com/go-pkgz/jrpc v0.3.0
|
||||
github.com/go-pkgz/lcw v1.0.2
|
||||
github.com/go-pkgz/lgr v0.11.0
|
||||
github.com/go-pkgz/notify v0.2.0
|
||||
github.com/go-pkgz/repeater v1.1.3
|
||||
github.com/go-pkgz/rest v1.15.0
|
||||
github.com/go-pkgz/syncs v1.2.0
|
||||
github.com/go-pkgz/rest v1.17.0
|
||||
github.com/go-pkgz/syncs v1.3.2
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
github.com/kyokomi/emoji/v2 v2.2.9
|
||||
github.com/microcosm-cc/bluemonday v1.0.18
|
||||
github.com/rakyll/statik v0.1.7
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/kyokomi/emoji/v2 v2.2.12
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.7.1
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
go.uber.org/goleak v1.1.12
|
||||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/image v0.11.0
|
||||
golang.org/x/net v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.6.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/go-chi/chi v4.1.1+incompatible // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.4.3 // indirect
|
||||
github.com/go-pkgz/email v0.3.0 // indirect
|
||||
github.com/go-pkgz/expirable-cache v0.0.3 // indirect
|
||||
github.com/go-redis/redis/v7 v7.4.1 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2 // indirect
|
||||
github.com/go-pkgz/email v0.4.1 // indirect
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/klauspost/compress v1.15.1 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/slack-go/slack v0.10.3 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/slack-go/slack v0.12.2 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.1 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||
go.mongodb.org/mongo-driver v1.9.1 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.1 // indirect
|
||||
golang.org/x/oauth2 v0.11.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+155
-657
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -119,12 +119,27 @@ GET {{host}}/api/v1/rss/reply?site={{site}}&user={{user}}
|
||||
GET {{host}}/api/v1/avatar/blah
|
||||
|
||||
### send confirmation token for current user to specified email. auth token for dev user for secret=12345.
|
||||
POST {{host}}/api/v1/email/subscribe?site={{site}}&address={{email}}
|
||||
### in case the user logged in with the same email, it will be confirmed right away with "updated" set to "true" in the response,
|
||||
### and no email will be sent.
|
||||
POST {{host}}/api/v1/email/subscribe
|
||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"site": "{{site}}",
|
||||
"address": "{{email}}"
|
||||
}
|
||||
|
||||
|
||||
### add email for notifications for current user via token from email. auth token for dev user for secret=12345.
|
||||
POST {{host}}/api/v1/email/confirm?site={{site}}&tkn={{token}}
|
||||
POST {{host}}/api/v1/email/confirm
|
||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"site": "{{site}}",
|
||||
"token": "{{token}}"
|
||||
}
|
||||
|
||||
### get current user email. auth token for dev user for secret=12345.
|
||||
GET {{host}}/api/v1/email?site={{site}}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
// Version is the current tagged release of the library.
|
||||
const Version = "1.23.0"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Changes
|
||||
|
||||
## [0.2.3](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.2...compute/metadata/v0.2.3) (2022-12-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compute/metadata:** Switch DNS lookup to an absolute lookup ([119b410](https://github.com/googleapis/google-cloud-go/commit/119b41060c7895e45e48aee5621ad35607c4d021)), refs [#7165](https://github.com/googleapis/google-cloud-go/issues/7165)
|
||||
|
||||
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.1...compute/metadata/v0.2.2) (2022-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compute/metadata:** Set IdleConnTimeout for http.Client ([#7084](https://github.com/googleapis/google-cloud-go/issues/7084)) ([766516a](https://github.com/googleapis/google-cloud-go/commit/766516aaf3816bfb3159efeea65aa3d1d205a3e2)), refs [#5430](https://github.com/googleapis/google-cloud-go/issues/5430)
|
||||
|
||||
## [0.1.0] (2022-10-26)
|
||||
|
||||
Initial release of metadata being it's own module.
|
||||
Generated
Vendored
+1
-1
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2014 Google Inc.
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Compute API
|
||||
|
||||
[](https://pkg.go.dev/cloud.google.com/go/compute/metadata)
|
||||
|
||||
This is a utility library for communicating with Google Cloud metadata service
|
||||
on Google Cloud.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get cloud.google.com/go/compute/metadata
|
||||
```
|
||||
|
||||
## Go Version Support
|
||||
|
||||
See the [Go Versions Supported](https://github.com/googleapis/google-cloud-go#go-versions-supported)
|
||||
section in the root directory's README.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please, see the [CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md)
|
||||
document for details.
|
||||
|
||||
Please note that this project is released with a Contributor Code of Conduct.
|
||||
By participating in this project you agree to abide by its terms. See
|
||||
[Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md#contributor-code-of-conduct)
|
||||
for more information.
|
||||
+4
-2
@@ -16,7 +16,7 @@
|
||||
// metadata and API service accounts.
|
||||
//
|
||||
// This package is a wrapper around the GCE metadata service,
|
||||
// as documented at https://developers.google.com/compute/docs/metadata.
|
||||
// as documented at https://cloud.google.com/compute/docs/metadata/overview.
|
||||
package metadata // import "cloud.google.com/go/compute/metadata"
|
||||
|
||||
import (
|
||||
@@ -70,7 +70,9 @@ func newDefaultHTTPClient() *http.Client {
|
||||
Timeout: 2 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
},
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +147,7 @@ func testOnGCE() bool {
|
||||
|
||||
go func() {
|
||||
resolver := &net.Resolver{}
|
||||
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal")
|
||||
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
|
||||
if err != nil || len(addrs) == 0 {
|
||||
resc <- false
|
||||
return
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file, and the {{.RootMod}} import, won't actually become part of
|
||||
// the resultant binary.
|
||||
//go:build modhack
|
||||
// +build modhack
|
||||
|
||||
package metadata
|
||||
|
||||
// Necessary for safely adding multi-module repo. See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
|
||||
import _ "cloud.google.com/go/compute/internal"
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- govet
|
||||
disable:
|
||||
- errcheck
|
||||
- maligned
|
||||
- prealloc
|
||||
disable-all: false
|
||||
presets:
|
||||
- bugs
|
||||
- unused
|
||||
fast: false
|
||||
Generated
Vendored
+10
-9
@@ -5,7 +5,7 @@ type: docker
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.13.1
|
||||
image: golang:latest
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
@@ -13,13 +13,14 @@ steps:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
- name: linter
|
||||
image: golang:1.13.1
|
||||
image: golang:latest
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
- name: deps
|
||||
path: /go
|
||||
commands:
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.20.0
|
||||
- ./bin/golangci-lint run
|
||||
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s
|
||||
- ./bin/golangci-lint run --timeout 5m
|
||||
- rm -r ./bin/
|
||||
|
||||
- name: coverage
|
||||
image: plugins/codecov
|
||||
@@ -37,8 +38,8 @@ steps:
|
||||
token:
|
||||
from_secret: telegram_token
|
||||
message: >
|
||||
*{{repo.name}}*
|
||||
[Build {{build.number}}]({{build.link}}) by {{commit.author}} {{#success build.status}}succeeded{{else}}failed{{/success}} in {{buildtime build.started}}
|
||||
*{{repo.name}}*
|
||||
[Build {{build.number}}]({{build.link}}) by {{commit.author}} {{#success build.status}}succeeded{{else}}failed{{/success}} in {{buildtime build.started}}
|
||||
`{{truncate commit.sha 8}}`: "{{commit.message}}"
|
||||
when:
|
||||
status:
|
||||
@@ -48,4 +49,4 @@ steps:
|
||||
volumes:
|
||||
- name: deps
|
||||
host:
|
||||
path: /var/lib/cache/godeps/
|
||||
path: /var/lib/cache/godeps/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user