example scenarios

This commit is contained in:
Roni Dover 2023-07-10 20:57:33 -07:00
parent 0d9e882e54
commit 2163b7c7f8
44 changed files with 1177 additions and 411 deletions

19
Dockerfile Normal file
View file

@ -0,0 +1,19 @@
FROM eclipse-temurin:17-jre
EXPOSE 8082
ENV OTEL_SERVICE_NAME=PetClinic
ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:5050
ENV OTEL_LOGS_EXPORTER="otlp"
ENV OTEL_METRICS_EXPORTER=none
ENV CODE_PACKAGE_PREFIXES="org.springframework.samples.petclinic"
ENV DEPLOYMENT_ENV="SAMPLE_ENV"
ADD target/spring-petclinic-*.jar /app.jar
ADD build/otel/opentelemetry-javaagent.jar /opentelemetry-javaagent.jar
ADD build/otel/digma-otel-agent-extension.jar /digma-otel-agent-extension.jar
HEALTHCHECK --interval=20s --timeout=3s --start-period=10s --retries=4 \
CMD curl -f http://localhost:8082/ || exit 1
ENTRYPOINT java -jar -javaagent:/opentelemetry-javaagent.jar -Dotel.javaagent.extensions=/digma-otel-agent-extension.jar app.jar

Binary file not shown.

View file

@ -0,0 +1,15 @@
version: '3'
services:
pet-clinic:
volumes:
- "./otel/opentelemetry-javaagent.jar:/otel/opentelemetry-javaagent.jar"
- "./otel/digma-otel-agent-extension.jar:/otel/digma-otel-agent-extension.jar"
environment:
- JAVA_TOOL_OPTIONS=-javaagent:/otel/opentelemetry-javaagent.jar -Dotel.exporter.otlp.endpoint=http://host.docker.internal:5050 -Dotel.javaagent.extensions=/otel/digma-otel-agent-extension.jar
- OTEL_SERVICE_NAME=pet-clinic
- DEPLOYMENT_ENV=LOCAL_DOCKER
extra_hosts:
- "host.docker.internal:host-gateway"

View file

@ -1,6 +1,18 @@
version: "2.2"
version: '3'
services:
pet-clinic:
build: ./
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8082/" ]
interval: 20s
timeout: 10s
retries: 4
start_period: 5s
ports:
- "8082:8082"
entrypoint: java -jar app.jar
mysql:
image: mysql:8.0
ports:

BIN
opentelemetry-javaagent.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

23
petshop-chart/.helmignore Normal file
View file

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

24
petshop-chart/Chart.yaml Normal file
View file

@ -0,0 +1,24 @@
apiVersion: v2
name: petshop-chart
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.3
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

View file

@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "petshop-chart.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "petshop-chart.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "petshop-chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "petshop-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View file

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "petshop-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "petshop-chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "petshop-chart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "petshop-chart.labels" -}}
helm.sh/chart: {{ include "petshop-chart.chart" . }}
{{ include "petshop-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "petshop-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "petshop-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "petshop-chart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "petshop-chart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,72 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "petshop-chart.fullname" . }}
labels:
{{- include "petshop-chart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "petshop-chart.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "petshop-chart.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "petshop-chart.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8082
protocol: TCP
env:
- name: "OTEL_SERVICE_NAME"
value: "PetClinicWithAgent"
- name: "OTEL_EXPORTER_OTLP_ENDPOINT"
value: "http://digma-collector-api.digma.svc.cluster.local:5050"
- name: "OTEL_LOGS_EXPORTER"
value: "otlp"
- name: "CODE_PACKAGE_PREFIXES"
value: "org.springframework.samples.petclinic"
- name: "DEPLOYMENT_ENV"
value: "Load test"
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -0,0 +1,28 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "petshop-chart.fullname" . }}
labels:
{{- include "petshop-chart.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "petshop-chart.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "petshop-chart.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "petshop-chart.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "petshop-chart.fullname" . }}
labels:
{{- include "petshop-chart.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: 8082
protocol: TCP
name: api
selector:
{{- include "petshop-chart.selectorLabels" . | nindent 4 }}

View file

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "petshop-chart.serviceAccountName" . }}
labels:
{{- include "petshop-chart.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "petshop-chart.fullname" . }}-test-connection"
labels:
{{- include "petshop-chart.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "petshop-chart.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

82
petshop-chart/values.yaml Normal file
View file

@ -0,0 +1,82 @@
# Default values for petshop-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: digmaai/petshop-app-deploy
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: 0.5.5
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: LoadBalancer
port: 80
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

22
pom.xml
View file

@ -83,7 +83,11 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
<!-- caching -->
<dependency>
<groupId>javax.cache</groupId>
@ -106,7 +110,11 @@
<version>${webjars-font-awesome.version}</version>
</dependency>
<!-- end of webjars -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
@ -117,6 +125,16 @@
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.26.0</version>
</dependency>
<dependency>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
<version>0.0.20131108.vaadin1</version>
</dependency>
</dependencies>

View file

@ -0,0 +1,14 @@
package org.springframework.samples.petclinic.adapters;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.json.JSONException;
import java.io.IOException;
public interface PetVaccinationService {
@WithSpan
VaccinnationRecord[] AllVaccines() throws JSONException, IOException;
@WithSpan
VaccinnationRecord VaccineRecord(int vaccinationRecordId) throws JSONException, IOException;
}

View file

@ -0,0 +1,74 @@
package org.springframework.samples.petclinic.adapters;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
@Component
public class PetVaccinationServiceFacade implements PetVaccinationService {
public static final String VACCINES_RECORDS_URL = "https://647f4bb4c246f166da9084c7.mockapi.io/api/vetcheck/vaccines";
private String MakeHttpCall(String url) throws IOException{
Request getAllVaccinesRequest = new Request.Builder().url(url).build();
OkHttpClient client = new OkHttpClient();
Response getAllVaccinesResult = client.newCall(getAllVaccinesRequest).execute();
return getAllVaccinesResult.body().string();
}
@Override
@WithSpan
public VaccinnationRecord[] AllVaccines() throws JSONException, IOException {
var vaccineListString = MakeHttpCall(VACCINES_RECORDS_URL);
JSONArray jArr = new JSONArray(vaccineListString);
var vaccinnationRecords =
new ArrayList<VaccinnationRecord>();
for (int i = 0; i < jArr.length(); i++) {
VaccinnationRecord record = parseVaccinationRecord(jArr.getJSONObject(i));
vaccinnationRecords.add(record);
}
return vaccinnationRecords.toArray(VaccinnationRecord[]::new);
}
@Override
@WithSpan
public VaccinnationRecord VaccineRecord(int vaccinationRecordId) throws JSONException, IOException {
var idUrl = VACCINES_RECORDS_URL + "/" + vaccinationRecordId;
var vaccineListString = MakeHttpCall(idUrl);
JSONObject vaccineJson = new JSONObject(vaccineListString);
return parseVaccinationRecord(vaccineJson);
}
@NotNull
private static VaccinnationRecord parseVaccinationRecord(JSONObject jsonObject) throws JSONException {
Integer petId = jsonObject.getInt("pet_id");
Integer id = jsonObject.getInt("id");
String vaccineDateString = jsonObject.getString("vaccine_date");
var vaccineDate = Instant.parse(vaccineDateString);
return new VaccinnationRecord(id, petId, vaccineDate);
}
}

View file

@ -0,0 +1,5 @@
package org.springframework.samples.petclinic.adapters;
public record VaccinnationRecord(int recordId, int petId, java.time.Instant vaccineDate) {
}

View file

@ -0,0 +1,172 @@
package org.springframework.samples.petclinic.domain;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.springframework.samples.petclinic.owner.Owner;
import java.util.concurrent.ThreadLocalRandom;
public class OwnerValidation {
private int counter = 0;
private UserValidationService usrValSvc;
private PasswordUtils pwdUtils;
private Tracer otelTracer;
private RoleService roleSvc;
private TwoFactorAuthenticationService twoFASvc;
public OwnerValidation(Tracer otelTracer) {
this.pwdUtils = new PasswordUtils();
this.roleSvc = new RoleService();
this.otelTracer = otelTracer;
this.usrValSvc = new UserValidationService();
this.twoFASvc = new TwoFactorAuthenticationService();
}
@WithSpan
public void ValidateOwnerWithExternalService(Owner owner) {
this.AuthServiceValidateUser(owner);
}
@WithSpan
private void NewFunction() {
}
@WithSpan
public boolean UserNameMustStartWithR(String usr) {
if (!usr.toLowerCase().startsWith("r")) {
return false;
}
return true;
}
@WithSpan
// This function and classes were generated by ChatGPT
public boolean ValidateUserAccess(String usr, String pswd, String sysCode) {
UserNameMustStartWithR(usr);
boolean vldUsr = usrValSvc.vldtUsr(usr);
if (!vldUsr) {
return false;
}
boolean vldPswd = pwdUtils.vldtPswd(usr, pswd);
if (!vldPswd) {
return false;
}
boolean vldUsrRole = roleSvc.vldtUsrRole(usr, sysCode);
if (!vldUsrRole) {
return false;
}
boolean is2FASuccess = twoFASvc.init2FA(usr);
if (!is2FASuccess) {
return false;
}
boolean is2FATokenValid = false;
int retry = 0;
while (retry < 3 && !is2FATokenValid) {
String token = twoFASvc.getTokenInput();
is2FATokenValid = twoFASvc.vldtToken(usr, token);
retry++;
}
if (!is2FATokenValid) {
return false;
}
return true;
}
@WithSpan
private synchronized void AuthServiceValidateUser(Owner owner) {
// This is the actual Root Cause!!
try {
Thread.sleep(4200 + ThreadLocalRandom.current().nextInt(90, 1100 + 1));
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@WithSpan
public boolean checkOwnerValidity(Owner owner) {
this.ValidateOwnerUserBad(owner);
return ValidateOwnerUser(owner);
}
@WithSpan
private boolean ValidateOwnerUserBad(Owner owner) {
{
for (int i = 0; i < 100; i++) {
ValidateOwner();
}
return true;
}
}
@WithSpan
private boolean ValidateOwnerUser(Owner owner) {
Span span = otelTracer.spanBuilder("db_access_01").startSpan();
var max = ThreadLocalRandom.current().nextInt(90, 110 + 1);
try {
for (int i = 0; i < max; i++) {
ValidateOwner();
}
}
finally {
span.end();
}
return true;
}
@WithSpan
private void ValidateOwner() {
// simulate SpanKind of DB query
// see
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md
Span span = otelTracer.spanBuilder("query_users_by_id")
.setSpanKind(SpanKind.CLIENT)
.setAttribute("db.system", "other_sql")
.setAttribute("db.statement", "select * from users where id = :id")
.startSpan();
try {
Thread.sleep(14);
}
catch (Exception e) {
}
finally {
span.end();
}
}
@WithSpan
public void PerformValidationFlow(Owner owner) {
}
}

View file

@ -0,0 +1,29 @@
package org.springframework.samples.petclinic.domain;
import io.opentelemetry.instrumentation.annotations.WithSpan;
public class PasswordUtils {
@WithSpan
public boolean vldtPswd(String usr, String pswd) {
try {
Thread.sleep(1);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
@WithSpan
public String encPswd(String pswd) {
try {
Thread.sleep(300);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "";
}
}

View file

@ -0,0 +1,48 @@
package org.springframework.samples.petclinic.domain;
import io.opentelemetry.api.trace.Span;
import org.json.JSONException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.adapters.PetVaccinationService;
import org.springframework.samples.petclinic.adapters.PetVaccinationServiceFacade;
import org.springframework.samples.petclinic.adapters.VaccinnationRecord;
import org.springframework.samples.petclinic.owner.Pet;
import org.springframework.samples.petclinic.owner.PetVaccine;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
@Component
public class PetVaccinationStatusService {
@Autowired
private PetVaccinationService adapter;
public void UpdateVaccinationStatus(Pet[] pets){
for (Pet pet: pets){
try {
var vaccinationRecords = this.adapter.AllVaccines();
for (VaccinnationRecord record : vaccinationRecords){
var recordInfo = this.adapter.VaccineRecord(record.recordId());
if (recordInfo.petId()==pet.getId()){
var date = LocalDateTime.ofInstant(recordInfo.vaccineDate(), ZoneId.systemDefault());
PetVaccine petVaccine = new PetVaccine();
petVaccine.setDate(date.toLocalDate());
pet.addVaccine(petVaccine);
}
}
} catch (JSONException |IOException e) {
//Fail silently
Span.current().recordException(e);
}
}
}
}

View file

@ -0,0 +1,18 @@
package org.springframework.samples.petclinic.domain;
import io.opentelemetry.instrumentation.annotations.WithSpan;
public class RoleService {
@WithSpan
public boolean vldtUsrRole(String usr, String sysCode) {
try {
Thread.sleep(40);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
}

View file

@ -0,0 +1,33 @@
package org.springframework.samples.petclinic.domain;
import io.opentelemetry.instrumentation.annotations.WithSpan;
public class TwoFactorAuthenticationService {
@WithSpan
public boolean init2FA(String usr) {
try {
Thread.sleep(400);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
public String getTokenInput() {
return "";
}
public boolean vldtToken(String usr, String token) {
try {
Thread.sleep(40);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
}

View file

@ -0,0 +1,19 @@
package org.springframework.samples.petclinic.domain;
import io.opentelemetry.instrumentation.annotations.WithSpan;
public class UserValidationService {
@WithSpan
public boolean vldtUsr(String usr) {
try {
Thread.sleep(300);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return true;
}
}

View file

@ -15,7 +15,11 @@
*/
package org.springframework.samples.petclinic.owner;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.core.style.ToStringCreator;
@ -124,6 +128,27 @@ public class Owner extends Person {
return null;
}
public boolean isVaccineExpired() {
for (Pet pet : getPets()) {
if (!pet.isNew()) {
for (PetVaccine vaccine : pet.getPetVaccines()) {
Duration duration = Duration.between(LocalDateTime.now(), vaccine.getDate());
if (duration.toDays() > 360) {
return true;
}
}
}
}
return false;
}
/**
* Return the Pet with the given name, or null if none found for this Owner.
* @param name to test

View file

@ -18,9 +18,11 @@ package org.springframework.samples.petclinic.owner;
import java.util.List;
import java.util.Map;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.domain.OwnerValidation;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@ -35,6 +37,8 @@ import org.springframework.web.servlet.ModelAndView;
import jakarta.validation.Valid;
import static io.opentelemetry.api.GlobalOpenTelemetry.getTracer;
/**
* @author Juergen Hoeller
* @author Ken Krebs
@ -48,8 +52,14 @@ class OwnerController {
private final OwnerRepository owners;
private OwnerValidation validator;
public OwnerController(OwnerRepository clinicService) {
this.owners = clinicService;
var otelTracer = getTracer("OwnerController");
validator = new OwnerValidation(otelTracer);
}
@InitBinder
@ -64,7 +74,10 @@ class OwnerController {
@GetMapping("/owners/new")
public String initCreationForm(Map<String, Object> model) {
Owner owner = new Owner();
validator.ValidateOwnerWithExternalService(owner);
model.put("owner", owner);
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@ -74,8 +87,12 @@ class OwnerController {
if (result.hasErrors()) {
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
validator.ValidateOwnerWithExternalService(owner);
validator.PerformValidationFlow(owner);
validator.checkOwnerValidity(owner);
this.owners.save(owner);
validator.ValidateUserAccess("admin", "pwd", "fullaccess");
return "redirect:/owners/" + owner.getId();
}
@ -87,6 +104,8 @@ class OwnerController {
@GetMapping("/owners")
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
Model model) {
validator.ValidateUserAccess("admin", "pwd", "fullaccess");
// allow parameterless GET request for /owners to return all records
if (owner.getLastName() == null) {
owner.setLastName(""); // empty string signifies broadest possible search
@ -110,6 +129,11 @@ class OwnerController {
return addPaginationModel(page, model, ownersResults);
}
private String addPaginationModel(int page, Model model, Page<Owner> paginated) {
model.addAttribute("listOwners", paginated);
List<Owner> listOwners = paginated.getContent();
@ -120,12 +144,18 @@ class OwnerController {
return "owners/ownersList";
}
@WithSpan
private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) {
int pageSize = 5;
int pageSize = 25;
Pageable pageable = PageRequest.of(page - 1, pageSize);
return owners.findByLastName(lastname, pageable);
}
@GetMapping("/owners/{ownerId}/edit")
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) {
Owner owner = this.owners.findById(ownerId);
@ -133,6 +163,24 @@ class OwnerController {
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/owners/{ownerId}/edit")
public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result,
@PathVariable("ownerId") int ownerId) {
@ -151,8 +199,10 @@ class OwnerController {
* @return a ModelMap with the model attributes for the view
*/
@GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails");
public ModelAndView showOwner(@PathVariable("ownerId")
int ownerId) {
ModelAndView mav =
new ModelAndView("owners/ownerDetails");
Owner owner = this.owners.findById(ownerId);
mav.addObject(owner);
return mav;

View file

@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.samples.petclinic.model.NamedEntity;
@ -57,6 +58,11 @@ public class Pet extends NamedEntity {
@OrderBy("visit_date ASC")
private Set<Visit> visits = new LinkedHashSet<>();
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "pet_id")
@OrderBy("vaccine_date ASC")
private Set<PetVaccine> pet_vaccines = new LinkedHashSet<>();
public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate;
}
@ -77,8 +83,16 @@ public class Pet extends NamedEntity {
return this.visits;
}
public Collection<PetVaccine> getPetVaccines() {
return this.pet_vaccines;
}
public void addVisit(Visit visit) {
getVisits().add(visit);
}
public void addVaccine(PetVaccine vaccine) {
getPetVaccines().add(vaccine);
}
}

View file

@ -17,6 +17,8 @@ package org.springframework.samples.petclinic.owner;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.domain.PetVaccinationStatusService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
@ -44,6 +46,9 @@ class PetController {
private final OwnerRepository owners;
@Autowired
private PetVaccinationStatusService petVaccinationStatus;
public PetController(OwnerRepository owners) {
this.owners = owners;
}
@ -82,6 +87,17 @@ class PetController {
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/new")
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model) {
if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) {
@ -95,9 +111,18 @@ class PetController {
}
this.owners.save(owner);
petVaccinationStatus.UpdateVaccinationStatus(owner.getPets().toArray(Pet[]::new));
return "redirect:/owners/{ownerId}";
}
@GetMapping("/pets/{petId}/edit")
public String initUpdateForm(Owner owner, @PathVariable("petId") int petId, ModelMap model) {
Pet pet = owner.getPet(petId);

View file

@ -0,0 +1,39 @@
package org.springframework.samples.petclinic.owner;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.samples.petclinic.model.BaseEntity;
import java.time.LocalDate;
/**
* Simple JavaBean domain object representing a visit.
*
* @author Ken Krebs
* @author Dave Syer
*/
@Entity
@Table(name = "pet_vaccines")
public class PetVaccine extends BaseEntity {
@Column(name = "vaccine_date")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate date;
/**
* Creates a new instance of Visit for the current date
*/
public PetVaccine() {
}
public LocalDate getDate() {
return this.date;
}
public void setDate(LocalDate date) {
this.date = date;
}
}

View file

@ -15,10 +15,18 @@
*/
package org.springframework.samples.petclinic.owner;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import java.io.IOException;
/**
* <code>Validator</code> for <code>Pet</code> forms.
* <p>
@ -51,6 +59,35 @@ public class PetValidator implements Validator {
if (pet.getBirthDate() == null) {
errors.rejectValue("birthDate", REQUIRED, REQUIRED);
}
try {
updateVaccinationStatus(pet);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
@WithSpan
private boolean updateVaccinationStatus(Pet pet) throws IOException {
// OkHttpClient client = new OkHttpClient();
//
// Request request = new
// Request.Builder().url("https://647f4bb4c246f166da9084c7.mockapi.io/api/vetcheck/vaccines")
// .build();
//
// String responseText = "";
// try (Response response = client.newCall(request).execute()) {
// responseText = response.body().string();
// JSONObject Jobject = new JSONObject(responseText);
// JSONArray Jarray = Jobject.getJSONArray("employees");
//
// }
// catch (JSONException e) {
// throw new RuntimeException(e);
// }
return true;
}
/**

View file

@ -1,5 +1,5 @@
database=postgres
spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/petclinic}
spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/postgres}
spring.datasource.username=${POSTGRES_USER:petclinic}
spring.datasource.password=${POSTGRES_PASS:petclinic}
# SQL is written to be idempotent so this is safe

View file

@ -15,6 +15,11 @@ spring.messages.basename=messages/messages
# Actuator
management.endpoints.web.exposure.include=*
management.metrics.distribution.slo.http.server.requests=50ms, 100ms, 200ms, 400ms
management.metrics.distribution.percentiles.http.server.requests=0.5, 0.9, 0.95, 0.99, 0.999
management.metrics.web.server.request.autotime.percentiles=0.95
management.metrics.distribution.percentiles-histogram.http.server.requests=true
# Logging
logging.level.org.springframework=INFO
@ -23,3 +28,4 @@ logging.level.org.springframework=INFO
# Maximum time static resources should be cached
spring.web.resources.cache.cachecontrol.max-age=12h
server.port=8082

View file

@ -34,6 +34,18 @@ INSERT INTO owners VALUES (default, 'David', 'Schroeder', '2749 Blackhawk Trail'
INSERT INTO owners VALUES (default, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487');
INSERT INTO pets VALUES (default, 'Leo', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Mocka', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Abby', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Tesla', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Freer', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Castro', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Mohawk', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Jerome', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Niley', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Timmy', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Bob', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Basil', '2012-08-06', 6, 2);
INSERT INTO pets VALUES (default, 'Rosy', '2011-04-17', 2, 3);
INSERT INTO pets VALUES (default, 'Jewel', '2010-03-07', 2, 3);

View file

@ -62,3 +62,12 @@ CREATE TABLE visits (
);
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);
CREATE TABLE pet_vaccines (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
pet_id INTEGER,
vaccine_date DATE
);
ALTER TABLE pet_vaccines ADD CONSTRAINT fk_pet_vaccines_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX pet_vaccines_pet_id ON pet_vaccines (pet_id);

View file

@ -50,3 +50,9 @@ CREATE TABLE IF NOT EXISTS visits (
description TEXT
);
CREATE INDEX ON visits (pet_id);
CREATE TABLE IF NOT EXISTS pet_vaccines (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
pet_id INT REFERENCES pets (id),
vaccine_date DATE,
);

View file

@ -26,6 +26,11 @@
<th>Telephone</th>
<td th:text="*{telephone}"></td>
</tr>
<tr>
<th>Needs Vaccine</th>
<td th:text="*{isVaccineExpired()}"></td>
</tr>
</table>
<a th:href="@{__${owner.id}__/edit}" class="btn btn-primary">Edit

View file

@ -1,126 +0,0 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* 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
*
* https://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 org.springframework.samples.petclinic.owner;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* Test class for the {@link PetController}
*
* @author Colin But
*/
@WebMvcTest(value = PetController.class,
includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE))
class PetControllerTests {
private static final int TEST_OWNER_ID = 1;
private static final int TEST_PET_ID = 1;
@Autowired
private MockMvc mockMvc;
@MockBean
private OwnerRepository owners;
@BeforeEach
void setup() {
PetType cat = new PetType();
cat.setId(3);
cat.setName("hamster");
given(this.owners.findPetTypes()).willReturn(Lists.newArrayList(cat));
Owner owner = new Owner();
Pet pet = new Pet();
owner.addPet(pet);
pet.setId(TEST_PET_ID);
given(this.owners.findById(TEST_OWNER_ID)).willReturn(owner);
}
@Test
void testInitCreationForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/new", TEST_OWNER_ID))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"))
.andExpect(model().attributeExists("pet"));
}
@Test
void testProcessCreationFormSuccess() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty")
.param("type", "hamster")
.param("birthDate", "2015-02-12"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test
void testProcessCreationFormHasErrors() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty")
.param("birthDate", "2015-02-12"))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(model().attributeHasFieldErrors("pet", "type"))
.andExpect(model().attributeHasFieldErrorCode("pet", "type", "required"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void testInitUpdateForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID))
.andExpect(status().isOk())
.andExpect(model().attributeExists("pet"))
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void testProcessUpdateFormSuccess() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty")
.param("type", "hamster")
.param("birthDate", "2015-02-12"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test
void testProcessUpdateFormHasErrors() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty")
.param("birthDate", "2015/02/12"))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
}

View file

@ -1,228 +0,0 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* 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
*
* https://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 org.springframework.samples.petclinic.service;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDate;
import java.util.Collection;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.owner.Owner;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.samples.petclinic.owner.Pet;
import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.owner.Visit;
import org.springframework.samples.petclinic.vet.Vet;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Integration test of the Service and the Repository layer.
* <p>
* ClinicServiceSpringDataJpaTests subclasses benefit from the following services provided
* by the Spring TestContext Framework:
* </p>
* <ul>
* <li><strong>Spring IoC container caching</strong> which spares us unnecessary set up
* time between test execution.</li>
* <li><strong>Dependency Injection</strong> of test fixture instances, meaning that we
* don't need to perform application context lookups. See the use of
* {@link Autowired @Autowired} on the <code> </code> instance variable, which uses
* autowiring <em>by type</em>.
* <li><strong>Transaction management</strong>, meaning each test method is executed in
* its own transaction, which is automatically rolled back by default. Thus, even if tests
* insert or otherwise change database state, there is no need for a teardown or cleanup
* script.
* <li>An {@link org.springframework.context.ApplicationContext ApplicationContext} is
* also inherited and can be used for explicit bean lookup if necessary.</li>
* </ul>
*
* @author Ken Krebs
* @author Rod Johnson
* @author Juergen Hoeller
* @author Sam Brannen
* @author Michael Isvy
* @author Dave Syer
*/
@DataJpaTest(includeFilters = @ComponentScan.Filter(Service.class))
// Ensure that if the mysql profile is active we connect to the real database:
@AutoConfigureTestDatabase(replace = Replace.NONE)
// @TestPropertySource("/application-postgres.properties")
class ClinicServiceTests {
@Autowired
protected OwnerRepository owners;
@Autowired
protected VetRepository vets;
Pageable pageable;
@Test
void shouldFindOwnersByLastName() {
Page<Owner> owners = this.owners.findByLastName("Davis", pageable);
assertThat(owners).hasSize(2);
owners = this.owners.findByLastName("Daviss", pageable);
assertThat(owners).isEmpty();
}
@Test
void shouldFindSingleOwnerWithPet() {
Owner owner = this.owners.findById(1);
assertThat(owner.getLastName()).startsWith("Franklin");
assertThat(owner.getPets()).hasSize(1);
assertThat(owner.getPets().get(0).getType()).isNotNull();
assertThat(owner.getPets().get(0).getType().getName()).isEqualTo("cat");
}
@Test
@Transactional
void shouldInsertOwner() {
Page<Owner> owners = this.owners.findByLastName("Schultz", pageable);
int found = (int) owners.getTotalElements();
Owner owner = new Owner();
owner.setFirstName("Sam");
owner.setLastName("Schultz");
owner.setAddress("4, Evans Street");
owner.setCity("Wollongong");
owner.setTelephone("4444444444");
this.owners.save(owner);
assertThat(owner.getId().longValue()).isNotEqualTo(0);
owners = this.owners.findByLastName("Schultz", pageable);
assertThat(owners.getTotalElements()).isEqualTo(found + 1);
}
@Test
@Transactional
void shouldUpdateOwner() {
Owner owner = this.owners.findById(1);
String oldLastName = owner.getLastName();
String newLastName = oldLastName + "X";
owner.setLastName(newLastName);
this.owners.save(owner);
// retrieving new name from database
owner = this.owners.findById(1);
assertThat(owner.getLastName()).isEqualTo(newLastName);
}
@Test
void shouldFindAllPetTypes() {
Collection<PetType> petTypes = this.owners.findPetTypes();
PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1);
assertThat(petType1.getName()).isEqualTo("cat");
PetType petType4 = EntityUtils.getById(petTypes, PetType.class, 4);
assertThat(petType4.getName()).isEqualTo("snake");
}
@Test
@Transactional
void shouldInsertPetIntoDatabaseAndGenerateId() {
Owner owner6 = this.owners.findById(6);
int found = owner6.getPets().size();
Pet pet = new Pet();
pet.setName("bowser");
Collection<PetType> types = this.owners.findPetTypes();
pet.setType(EntityUtils.getById(types, PetType.class, 2));
pet.setBirthDate(LocalDate.now());
owner6.addPet(pet);
assertThat(owner6.getPets().size()).isEqualTo(found + 1);
this.owners.save(owner6);
owner6 = this.owners.findById(6);
assertThat(owner6.getPets().size()).isEqualTo(found + 1);
// checks that id has been generated
pet = owner6.getPet("bowser");
assertThat(pet.getId()).isNotNull();
}
@Test
@Transactional
void shouldUpdatePetName() throws Exception {
Owner owner6 = this.owners.findById(6);
Pet pet7 = owner6.getPet(7);
String oldName = pet7.getName();
String newName = oldName + "X";
pet7.setName(newName);
this.owners.save(owner6);
owner6 = this.owners.findById(6);
pet7 = owner6.getPet(7);
assertThat(pet7.getName()).isEqualTo(newName);
}
@Test
void shouldFindVets() {
Collection<Vet> vets = this.vets.findAll();
Vet vet = EntityUtils.getById(vets, Vet.class, 3);
assertThat(vet.getLastName()).isEqualTo("Douglas");
assertThat(vet.getNrOfSpecialties()).isEqualTo(2);
assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("dentistry");
assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("surgery");
}
@Test
@Transactional
void shouldAddNewVisitForPet() {
Owner owner6 = this.owners.findById(6);
Pet pet7 = owner6.getPet(7);
int found = pet7.getVisits().size();
Visit visit = new Visit();
visit.setDescription("test");
owner6.addVisit(pet7.getId(), visit);
this.owners.save(owner6);
owner6 = this.owners.findById(6);
assertThat(pet7.getVisits()) //
.hasSize(found + 1) //
.allMatch(value -> value.getId() != null);
}
@Test
void shouldFindVisitsByPetId() throws Exception {
Owner owner6 = this.owners.findById(6);
Pet pet7 = owner6.getPet(7);
Collection<Visit> visits = pet7.getVisits();
assertThat(visits) //
.hasSize(2) //
.element(0)
.extracting(Visit::getDate)
.isNotNull();
}
}

View file

@ -9,12 +9,12 @@
<collectionProp name="Arguments.arguments">
<elementProp name="PETCLINIC_HOST" elementType="Argument">
<stringProp name="Argument.name">PETCLINIC_HOST</stringProp>
<stringProp name="Argument.value">localhost</stringProp>
<stringProp name="Argument.value">a25c892af46df47589102bdef28fda12-1374796034.eu-west-1.elb.amazonaws.com</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="PETCLINIC_PORT" elementType="Argument">
<stringProp name="Argument.name">PETCLINIC_PORT</stringProp>
<stringProp name="Argument.value">8080</stringProp>
<stringProp name="Argument.value">80</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="CONTEXT_WEB" elementType="Argument">